feat: add project status and target filters to reports (#7345)

* feat: add project status and target filters

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
ThatGravyBoat
2026-09-01 00:10:01 +00:00
committed by GitHub
co-authored by Prospector
parent 19dc819821
commit 6b5f5e8065
2 changed files with 88 additions and 22 deletions
@@ -72,6 +72,13 @@
</span> </span>
</div> </div>
<div
v-if="report.project?.status"
class="flex items-center gap-1 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
>
<Badge :type="report.project?.status" class="text-sm" />
</div>
<span <span
v-if="report.item_type === 'version' && report.version" v-if="report.item_type === 'version' && report.version"
class="text-sm text-secondary" class="text-sm text-secondary"
@@ -229,6 +236,7 @@ import {
} from '@modrinth/assets' } from '@modrinth/assets'
import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation' import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation'
import { import {
Badge,
Button, Button,
ButtonLink, ButtonLink,
CollapsibleRegion, CollapsibleRegion,
@@ -53,11 +53,11 @@
<MultiSelect <MultiSelect
v-model="currentReporterOrProject" v-model="currentReporterOrProject"
:options="reporterOrProjectOptions" :options="reportFilterOptions"
:max-height="500" :max-height="500"
dropdown-min-width="360px" dropdown-min-width="360px"
no-options-message="no options found" no-options-message="no options found"
:searchable="reporterOrProjectOptions.length > 6" :searchable="reportFilterOptions.length > 6"
:max-tag-rows="1" :max-tag-rows="1"
fit-content fit-content
trigger-type="base" trigger-type="base"
@@ -86,6 +86,20 @@
</div> </div>
</template> </template>
<template #top> <template #top>
<div
class="flex flex-row justify-between gap-1 border-0 border-b border-solid border-b-surface-5 py-2"
>
<div
v-for="section in hiddenSections"
:key="section.key"
class="flex flex-row gap-2 px-4"
>
<span class="font-semibold text-primary">
{{ section.key }}
</span>
<Checkbox v-model="section.enabled" />
</div>
</div>
<div> <div>
<button <button
type="button" type="button"
@@ -208,8 +222,9 @@ import {
SortAscIcon, SortAscIcon,
SortDescIcon, SortDescIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import type { ExtendedReport } from '@modrinth/moderation' import type { ExtendedReport, OwnershipTarget } from '@modrinth/moderation'
import { import {
Checkbox,
Combobox, Combobox,
type ComboboxOption, type ComboboxOption,
commonMessages, commonMessages,
@@ -467,25 +482,48 @@ watch(
watch(() => route.query, readFiltersFromRoute, { deep: true }) watch(() => route.query, readFiltersFromRoute, { deep: true })
type FilterSection = {
key: 'project' | 'reporter' | 'target'
enabled: boolean
}
const hiddenSections = ref<FilterSection[]>([
{ key: 'project', enabled: true },
{ key: 'reporter', enabled: true },
{ key: 'target', enabled: true },
])
type ReportedType<T> = T & { report_item_count: number } type ReportedType<T> = T & { report_item_count: number }
const reporterOrProjectOptions = computed<MultiSelectItem<string>[]>(() => { const reportFilterOptions = computed<MultiSelectItem<string>[]>(() => {
if (!allReports.value) return [] if (!allReports.value) return []
const options: MultiSelectItem<string>[] = [] const options: MultiSelectItem<string>[] = []
const uniqueProjectIds: { [id: string]: ReportedType<Labrinth.Projects.v2.Project> } = {} const uniqueProjectIds: { [id: string]: ReportedType<Labrinth.Projects.v2.Project> } = {}
const uniqueReporterIds: { [id: string]: ReportedType<User> } = {} const uniqueReporterIds: { [id: string]: ReportedType<User> } = {}
const uniqueTargetIds: { [id: string]: ReportedType<OwnershipTarget> } = {}
const projectsEnabled =
hiddenSections.value.find((section) => section.key === 'project')?.enabled ?? true
const reportersEnabled =
hiddenSections.value.find((section) => section.key === 'reporter')?.enabled ?? true
const targetsEnabled =
hiddenSections.value.find((section) => section.key === 'target')?.enabled ?? true
for (const report of filteredReports.value) { for (const report of filteredReports.value) {
if (report.project) if (report.project && projectsEnabled)
uniqueProjectIds[report.project.id] = { uniqueProjectIds[report.project.id] = {
...report.project, ...report.project,
report_item_count: (uniqueProjectIds[report.project.id]?.report_item_count || 0) + 1, report_item_count: (uniqueProjectIds[report.project.id]?.report_item_count || 0) + 1,
} }
if (report.reporter_user) if (report.reporter_user && reportersEnabled)
uniqueReporterIds[report.reporter_user.id] = { uniqueReporterIds[report.reporter_user.id] = {
...report.reporter_user, ...report.reporter_user,
report_item_count: (uniqueReporterIds[report.reporter_user.id]?.report_item_count || 0) + 1, report_item_count: (uniqueReporterIds[report.reporter_user.id]?.report_item_count || 0) + 1,
} }
if (report.target && targetsEnabled)
uniqueTargetIds[report.target.slug] = {
...report.target,
report_item_count: (uniqueTargetIds[report.target.slug]?.report_item_count || 0) + 1,
}
} }
if (Object.keys(uniqueProjectIds).length !== 0) { if (Object.keys(uniqueProjectIds).length !== 0) {
@@ -505,20 +543,39 @@ const reporterOrProjectOptions = computed<MultiSelectItem<string>[]>(() => {
}) })
} }
options.push({ type: 'section-header', label: 'Reporters' }) if (Object.keys(uniqueReporterIds).length !== 0) {
Object.values(uniqueReporterIds) options.push({ type: 'section-header', label: 'Reporters' })
.sort((a, b) => Object.values(uniqueReporterIds)
a.report_item_count === b.report_item_count .sort((a, b) =>
? a.username.localeCompare(b.username) a.report_item_count === b.report_item_count
: b.report_item_count - a.report_item_count, ? a.username.localeCompare(b.username)
) : b.report_item_count - a.report_item_count,
.forEach((reporter) => { )
options.push({ .forEach((reporter) => {
value: `reporter/${reporter.id}`, options.push({
label: `${reporter.username} (${formatNumber(reporter.report_item_count)})`, value: `reporter/${reporter.id}`,
icon: reporter.avatar_url ? h('img', { src: reporter.avatar_url }) : undefined, label: `${reporter.username} (${formatNumber(reporter.report_item_count)})`,
icon: reporter.avatar_url ? h('img', { src: reporter.avatar_url }) : undefined,
})
}) })
}) }
if (Object.keys(uniqueTargetIds).length !== 0) {
options.push({ type: 'section-header', label: 'Targets' })
Object.values(uniqueTargetIds)
.sort((a, b) =>
a.report_item_count === b.report_item_count
? a.name.localeCompare(b.name)
: b.report_item_count - a.report_item_count,
)
.forEach((target) => {
options.push({
value: `target/${target.slug}`,
label: `${target.name} (${formatNumber(target.report_item_count)})`,
icon: target.avatar_url ? h('img', { src: target.avatar_url }) : undefined,
})
})
}
return options return options
}) })
@@ -753,12 +810,13 @@ const sortedReports = computed(() => {
reporterOrProjectFilter.length === 0 reporterOrProjectFilter.length === 0
? [...filteredReports.value] ? [...filteredReports.value]
: filteredReports.value.filter((report) => { : filteredReports.value.filter((report) => {
const reporterOrProjectFilterLookup = new Set(reporterOrProjectFilter) const lookup = new Set(reporterOrProjectFilter)
const reporterValue = report.reporter_user ? `reporter/${report.reporter_user.id}` : null const reporterValue = report.reporter_user ? `reporter/${report.reporter_user.id}` : null
const projectValue = report.project ? `project/${report.project.id}` : null const projectValue = report.project ? `project/${report.project.id}` : null
const targetValue = report.target ? `target/${report.target.slug}` : null
return ( return (
(reporterValue && reporterOrProjectFilterLookup.has(reporterValue)) || (reporterValue && lookup.has(reporterValue)) ||
(projectValue && reporterOrProjectFilterLookup.has(projectValue)) (projectValue && lookup.has(projectValue)) | (targetValue && lookup.has(targetValue))
) )
}) })