mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
feat: change project id filter to moderate by id
This commit is contained in:
+4
-13
@@ -5,14 +5,12 @@ import { nextTick, ref, useTemplateRef } from 'vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
apply: [projectIds: string[]]
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
|
||||
const textareaRef = useTemplateRef<InstanceType<typeof Textarea>>('textareaRef')
|
||||
const input = ref('')
|
||||
const error = ref('')
|
||||
let applied = false
|
||||
|
||||
function parseProjectIds(value: string): string[] {
|
||||
return [
|
||||
@@ -25,10 +23,9 @@ function parseProjectIds(value: string): string[] {
|
||||
]
|
||||
}
|
||||
|
||||
async function show(projectIds: string[]) {
|
||||
input.value = projectIds.join('\n')
|
||||
async function show() {
|
||||
input.value = ''
|
||||
error.value = ''
|
||||
applied = false
|
||||
modalRef.value?.show()
|
||||
await nextTick()
|
||||
textareaRef.value?.focus()
|
||||
@@ -38,10 +35,6 @@ function hide() {
|
||||
modalRef.value?.hide()
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
if (!applied) emit('cancel')
|
||||
}
|
||||
|
||||
function apply() {
|
||||
const projectIds = parseProjectIds(input.value)
|
||||
if (projectIds.length === 0) {
|
||||
@@ -49,7 +42,6 @@ function apply() {
|
||||
return
|
||||
}
|
||||
|
||||
applied = true
|
||||
emit('apply', projectIds)
|
||||
hide()
|
||||
}
|
||||
@@ -60,15 +52,14 @@ defineExpose({ show, hide })
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modalRef"
|
||||
header="Filter by project IDs"
|
||||
header="Moderate by IDs"
|
||||
width="36rem"
|
||||
max-width="calc(100vw - 2rem)"
|
||||
:on-hide="handleHide"
|
||||
>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="apply">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-semibold text-contrast" for="moderation-project-ids">
|
||||
Project IDs
|
||||
Project IDs to moderate
|
||||
</label>
|
||||
<Textarea
|
||||
id="moderation-project-ids"
|
||||
@@ -34,16 +34,6 @@
|
||||
</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
<IconButton
|
||||
v-if="isProjectIdsFilter"
|
||||
v-tooltip="'Edit project IDs'"
|
||||
label="Edit project IDs"
|
||||
size="lg"
|
||||
class="flex-shrink-0"
|
||||
@click="editProjectIdsFilter"
|
||||
>
|
||||
<EditIcon aria-hidden="true" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Combobox
|
||||
@@ -84,6 +74,15 @@
|
||||
</Combobox>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="outlined"
|
||||
class="flex !h-[40px] w-full items-center justify-center gap-2 sm:w-auto"
|
||||
@click="openModerateByIdsModal"
|
||||
>
|
||||
<ListFilterIcon class="flex-shrink-0" />
|
||||
Moderate by IDs
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="colored"
|
||||
color="orange"
|
||||
@@ -126,11 +125,7 @@
|
||||
:skipped-ids="moderationQueue.currentQueue.skipped"
|
||||
@review-skipped="reviewSkippedQueue"
|
||||
/>
|
||||
<ProjectIdsFilterModal
|
||||
ref="projectIdsFilterModal"
|
||||
@apply="applyProjectIdsFilter"
|
||||
@cancel="cancelProjectIdsFilter"
|
||||
/>
|
||||
<ModerateByIdsModal ref="moderateByIdsModal" @apply="startModeratingByIds" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
@@ -172,7 +167,6 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
CopyIcon,
|
||||
EditIcon,
|
||||
ListFilterIcon,
|
||||
ScaleIcon,
|
||||
SearchIcon,
|
||||
@@ -188,7 +182,6 @@ import {
|
||||
EmptyState,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Toggle,
|
||||
@@ -199,15 +192,14 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import ConfettiExplosion from 'vue-confetti-explosion'
|
||||
|
||||
import ModerateByIdsModal from '~/components/ui/moderation/ModerateByIdsModal.vue'
|
||||
import ModerationQueueCard from '~/components/ui/moderation/ModerationQueueCard.vue'
|
||||
import ProjectIdsFilterModal from '~/components/ui/moderation/ProjectIdsFilterModal.vue'
|
||||
import QueueSummaryModal from '~/components/ui/moderation/QueueSummaryModal.vue'
|
||||
import { type ModerationProject, toModerationProjects } from '~/helpers/moderation.ts'
|
||||
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
|
||||
import { useModerationQueue } from '~/services/moderation/queue.ts'
|
||||
import { findNextEligibleQueueProject } from '~/services/moderation/queue-eligibility.ts'
|
||||
import {
|
||||
fetchAllModerationQueueProjects,
|
||||
scanProjectsWithValidationIssues,
|
||||
type ValidationFilterRequest,
|
||||
} from '~/services/moderation/validation-filter.ts'
|
||||
@@ -223,10 +215,9 @@ const router = useRouter()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const debugValidationFilter = useDebugLogger('moderation-validation-filter')
|
||||
const debugProjectIdsFilter = useDebugLogger('moderation-project-ids-filter')
|
||||
|
||||
const queueSummaryModal = ref()
|
||||
const projectIdsFilterModal = ref<InstanceType<typeof ProjectIdsFilterModal>>()
|
||||
const moderateByIdsModal = ref<InstanceType<typeof ModerateByIdsModal>>()
|
||||
|
||||
const visible = ref(false)
|
||||
if (import.meta.client && history && history.state && history.state.confetti) {
|
||||
@@ -305,7 +296,6 @@ const filterTypes: ComboboxOption<string>[] = [
|
||||
{ value: 'Plugins', label: 'Plugins' },
|
||||
{ value: 'Shaders', label: 'Shaders' },
|
||||
{ value: 'Servers', label: 'Servers' },
|
||||
{ value: 'Project IDs', label: 'Project IDs' },
|
||||
{ value: 'Validation errors', label: 'Validation errors' },
|
||||
{ value: 'Validation errors + warnings', label: 'Validation errors + warnings' },
|
||||
{ value: 'Fucked up', label: 'Fucked up' },
|
||||
@@ -314,7 +304,6 @@ const filterTypeValues = filterTypes.map((option) => option.value)
|
||||
const DEFAULT_FILTER_TYPE = filterTypeValues[0]
|
||||
|
||||
const MODPACK_FILTER_TYPE = 'Modpacks'
|
||||
const PROJECT_IDS_FILTER_TYPE = 'Project IDs'
|
||||
const VALIDATION_ERROR_FILTER_TYPE = 'Validation errors'
|
||||
const VALIDATION_ERROR_AND_WARNING_FILTER_TYPE = 'Validation errors + warnings'
|
||||
const VALIDATION_FILTER_STALE_TIME_MS = 1000 * 60 * 5
|
||||
@@ -367,13 +356,10 @@ function parseSortTypeFromQuery(
|
||||
|
||||
const currentFilterType = ref(parseFilterTypeFromQuery(route.query.filter))
|
||||
const currentSortType = ref(parseSortTypeFromQuery(route.query.sort, currentFilterType.value))
|
||||
const projectIds = ref<string[]>([])
|
||||
const previousFilterType = ref(DEFAULT_FILTER_TYPE)
|
||||
const projectIdsSelectionPending = ref(false)
|
||||
|
||||
watch(
|
||||
currentFilterType,
|
||||
async (newFilter, oldFilter) => {
|
||||
(newFilter) => {
|
||||
if (
|
||||
newFilter !== MODPACK_FILTER_TYPE &&
|
||||
modpackSortTypeValues.includes(currentSortType.value)
|
||||
@@ -392,27 +378,10 @@ watch(
|
||||
path: route.path,
|
||||
query: currentQuery,
|
||||
})
|
||||
|
||||
if (newFilter === PROJECT_IDS_FILTER_TYPE && oldFilter !== PROJECT_IDS_FILTER_TYPE) {
|
||||
previousFilterType.value = oldFilter || DEFAULT_FILTER_TYPE
|
||||
projectIdsSelectionPending.value = true
|
||||
debugProjectIdsFilter('Project IDs filter selected; opening ID editor')
|
||||
await nextTick()
|
||||
projectIdsFilterModal.value?.show(projectIds.value)
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (currentFilterType.value !== PROJECT_IDS_FILTER_TYPE) return
|
||||
|
||||
projectIdsSelectionPending.value = true
|
||||
debugProjectIdsFilter('Project IDs filter loaded from URL; opening ID editor')
|
||||
await nextTick()
|
||||
projectIdsFilterModal.value?.show(projectIds.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.filter,
|
||||
(newFilterParam) => {
|
||||
@@ -528,7 +497,6 @@ const isValidationErrorAndWarningFilter = computed(
|
||||
const isValidationFilter = computed(
|
||||
() => isValidationErrorFilter.value || isValidationErrorAndWarningFilter.value,
|
||||
)
|
||||
const isProjectIdsFilter = computed(() => currentFilterType.value === PROJECT_IDS_FILTER_TYPE)
|
||||
|
||||
const {
|
||||
data: standardProjectsResponse,
|
||||
@@ -539,7 +507,7 @@ const {
|
||||
queryKey: moderationProjectsQueryKey,
|
||||
queryFn: ({ queryKey }) => client.labrinth.moderation_internal.getProjects(queryKey[1]),
|
||||
placeholderData: (previousData) => previousData,
|
||||
enabled: computed(() => !isValidationFilter.value && !isProjectIdsFilter.value),
|
||||
enabled: computed(() => !isValidationFilter.value),
|
||||
})
|
||||
|
||||
const validationFilterRequest = computed<ValidationFilterRequest>(() => ({
|
||||
@@ -620,88 +588,17 @@ watch([isValidationFilter, validationProjectsQueryKey], ([isActive]) => {
|
||||
}
|
||||
})
|
||||
|
||||
const projectIdsFilterRequest = computed<ValidationFilterRequest>(() => ({
|
||||
exclude_technical_review: excludeTechnicalReview.value,
|
||||
query: debouncedFilterQuery.value || undefined,
|
||||
sort: 'oldest',
|
||||
}))
|
||||
|
||||
const projectIdsProjectsQueryKey = computed(
|
||||
() =>
|
||||
[
|
||||
'moderation-projects',
|
||||
'project-ids',
|
||||
projectIdsFilterRequest.value,
|
||||
projectIds.value,
|
||||
] as const,
|
||||
)
|
||||
|
||||
const {
|
||||
data: projectIdsProjectsResponse,
|
||||
isPending: projectIdsProjectsPending,
|
||||
error: projectIdsProjectsError,
|
||||
} = useQuery({
|
||||
queryKey: projectIdsProjectsQueryKey,
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const requestedIds = queryKey[3]
|
||||
debugProjectIdsFilter(`Filtering queue for ${requestedIds.length} project IDs`)
|
||||
const queueResponse = await fetchAllModerationQueueProjects({
|
||||
client,
|
||||
request: queryKey[2],
|
||||
signal,
|
||||
log: debugProjectIdsFilter,
|
||||
})
|
||||
const queueProjectsById = new Map(
|
||||
queueResponse.projects.map((project) => [project.id, project]),
|
||||
)
|
||||
const projects = requestedIds
|
||||
.map((projectId) => queueProjectsById.get(projectId))
|
||||
.filter((project): project is NonNullable<typeof project> => !!project)
|
||||
const missingProjectIds = requestedIds.filter((projectId) => !queueProjectsById.has(projectId))
|
||||
|
||||
debugProjectIdsFilter(
|
||||
`Project ID filter complete: ${projects.length} matched, ${missingProjectIds.length} not in queue`,
|
||||
)
|
||||
if (missingProjectIds.length > 0) {
|
||||
debugProjectIdsFilter(`IDs not found in queue: ${missingProjectIds.join(', ')}`)
|
||||
}
|
||||
|
||||
return {
|
||||
total: projects.length,
|
||||
projects,
|
||||
}
|
||||
},
|
||||
enabled: computed(
|
||||
() =>
|
||||
import.meta.client &&
|
||||
currentFilterType.value === PROJECT_IDS_FILTER_TYPE &&
|
||||
projectIds.value.length > 0,
|
||||
),
|
||||
staleTime: VALIDATION_FILTER_STALE_TIME_MS,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const usesLocalPagination = computed(() => isValidationFilter.value || isProjectIdsFilter.value)
|
||||
const usesLocalPagination = computed(() => isValidationFilter.value)
|
||||
const moderationProjectsResponse = computed(() =>
|
||||
isValidationFilter.value
|
||||
? validationProjectsResponse.value
|
||||
: isProjectIdsFilter.value
|
||||
? projectIdsProjectsResponse.value
|
||||
: standardProjectsResponse.value,
|
||||
isValidationFilter.value ? validationProjectsResponse.value : standardProjectsResponse.value,
|
||||
)
|
||||
const pending = computed(() =>
|
||||
isValidationFilter.value
|
||||
? validationProjectsPending.value
|
||||
: isProjectIdsFilter.value
|
||||
? projectIds.value.length > 0 && projectIdsProjectsPending.value
|
||||
: standardProjectsPending.value || standardProjectsPlaceholder.value,
|
||||
: standardProjectsPending.value || standardProjectsPlaceholder.value,
|
||||
)
|
||||
const loadError = computed(() =>
|
||||
isValidationFilter.value
|
||||
? validationProjectsError.value
|
||||
: isProjectIdsFilter.value
|
||||
? projectIdsProjectsError.value
|
||||
: standardProjectsError.value,
|
||||
isValidationFilter.value ? validationProjectsError.value : standardProjectsError.value,
|
||||
)
|
||||
const loadErrorMessage = computed(
|
||||
() => loadError.value?.message ?? 'An unknown error occurred while loading the moderation queue.',
|
||||
@@ -764,9 +661,6 @@ const emptyStateDescription = computed(() => {
|
||||
return 'Check that your search query is correct!'
|
||||
}
|
||||
if (currentFilterType.value !== DEFAULT_FILTER_TYPE) {
|
||||
if (isProjectIdsFilter.value) {
|
||||
return 'None of the selected projects are currently in the moderation queue.'
|
||||
}
|
||||
return `There are no ${currentFilterType.value.toLowerCase()} in the queue.`
|
||||
}
|
||||
return 'you will probably never see this but if you do, congrats!!! :D'
|
||||
@@ -776,28 +670,8 @@ function goToPage(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function editProjectIdsFilter() {
|
||||
projectIdsSelectionPending.value = false
|
||||
debugProjectIdsFilter('Opening Project IDs editor')
|
||||
projectIdsFilterModal.value?.show(projectIds.value)
|
||||
}
|
||||
|
||||
function applyProjectIdsFilter(ids: string[]) {
|
||||
projectIds.value = ids
|
||||
projectIdsSelectionPending.value = false
|
||||
goToPage(1)
|
||||
debugProjectIdsFilter(`Applied Project IDs filter with ${ids.length} unique IDs`)
|
||||
}
|
||||
|
||||
function cancelProjectIdsFilter() {
|
||||
if (!projectIdsSelectionPending.value) {
|
||||
debugProjectIdsFilter('Project IDs edit cancelled')
|
||||
return
|
||||
}
|
||||
|
||||
projectIdsSelectionPending.value = false
|
||||
debugProjectIdsFilter(`Project IDs selection cancelled; restoring ${previousFilterType.value}`)
|
||||
currentFilterType.value = previousFilterType.value
|
||||
function openModerateByIdsModal() {
|
||||
moderateByIdsModal.value?.show()
|
||||
}
|
||||
|
||||
async function findFirstEligibleProject(): Promise<string | null> {
|
||||
@@ -838,6 +712,23 @@ async function navigateToModerationProject(projectId: string) {
|
||||
})
|
||||
}
|
||||
|
||||
async function startModeratingByIds(projectIds: string[]) {
|
||||
await moderationQueue.setQueue(projectIds)
|
||||
|
||||
const targetProjectId = await findFirstEligibleProject()
|
||||
|
||||
if (!targetProjectId) {
|
||||
addNotification({
|
||||
title: 'No projects available',
|
||||
text: 'None of the provided projects are awaiting moderation or available to review.',
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await navigateToModerationProject(targetProjectId)
|
||||
}
|
||||
|
||||
async function getFilteredProjectIds(): Promise<string[]> {
|
||||
if (usesLocalPagination.value) {
|
||||
return filteredProjects.value.map((project) => project.project.id)
|
||||
|
||||
@@ -6,7 +6,7 @@ export type ValidationFilterRequest = Omit<
|
||||
'count' | 'offset' | 'project_type'
|
||||
>
|
||||
|
||||
export interface ModerationQueueFetchOptions {
|
||||
interface ModerationQueueFetchOptions {
|
||||
client: AbstractModrinthClient
|
||||
request: ValidationFilterRequest
|
||||
signal: AbortSignal
|
||||
@@ -103,29 +103,6 @@ async function fetchQueueProjects(
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllModerationQueueProjects(
|
||||
options: ModerationQueueFetchOptions,
|
||||
): Promise<Labrinth.Moderation.Internal.ProjectsResponse> {
|
||||
const pacedFetcher = createPacedFetcher(options.signal, options.log)
|
||||
|
||||
try {
|
||||
options.log('Starting moderation queue fetch')
|
||||
const response = await fetchQueueProjects(options, pacedFetcher.fetch)
|
||||
options.log(`Queue fetch complete: ${response.total} projects fetched`)
|
||||
return response
|
||||
} catch (error) {
|
||||
if (options.signal.aborted) {
|
||||
options.log('Queue fetch cancelled')
|
||||
} else {
|
||||
console.error(
|
||||
`[moderation-project-ids-filter] Queue fetch failed during ${pacedFetcher.getCurrentStage()}`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function scanProjectsWithValidationIssues({
|
||||
client,
|
||||
request,
|
||||
|
||||
Reference in New Issue
Block a user