You've already forked AstralRinth
feat: use multi select for moderation reports (#6312)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -8,15 +8,11 @@
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="flex-1 lg:max-w-52"
|
||||
input-class="h-[40px]"
|
||||
wrapper-class="flex-1"
|
||||
input-class="h-[40px] w-full"
|
||||
@input="goToPage(1)"
|
||||
/>
|
||||
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-stretch justify-end gap-2 sm:flex-row sm:items-center lg:flex-shrink-0"
|
||||
>
|
||||
@@ -56,6 +52,72 @@
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<MultiSelect
|
||||
v-model="currentReporterOrProject"
|
||||
:options="reporterOrProjectOptions"
|
||||
:max-height="500"
|
||||
dropdown-min-width="360px"
|
||||
no-options-message="no options found"
|
||||
:searchable="reporterOrProjectOptions.length > 6"
|
||||
:max-tag-rows="1"
|
||||
fit-content
|
||||
checkbox-position="right"
|
||||
show-selection-actions
|
||||
should-show-select-all
|
||||
@update:model-value="goToPage(1)"
|
||||
>
|
||||
<template #input-content="{ isOpen, openDirection }">
|
||||
<div class="flex min-h-7 min-w-0 max-w-full flex-1 items-center gap-1.5 pr-1">
|
||||
<LayersIcon class="size-5 shrink-0 text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate px-0.5 font-semibold text-primary">
|
||||
{{
|
||||
currentReporterOrProject.length === 0
|
||||
? 'All Reports'
|
||||
: `${currentReporterOrProject.length} selected`
|
||||
}}
|
||||
</span>
|
||||
<ChevronLeftIcon
|
||||
class="size-5 shrink-0 text-primary transition-transform duration-150"
|
||||
:class="
|
||||
isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #top>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-1.5 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
|
||||
:aria-selected="currentReporterOrProject.length === 0"
|
||||
:class="currentReporterOrProject.length === 0 ? 'text-contrast' : 'text-primary'"
|
||||
role="option"
|
||||
@click="
|
||||
() => {
|
||||
currentReporterOrProject = []
|
||||
goToPage(1)
|
||||
}
|
||||
"
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
>
|
||||
<LayersIcon
|
||||
class="h-5 w-5 shrink-0 text-primary"
|
||||
:class="currentReporterOrProject.length === 0 ? 'text-contrast' : 'text-primary'"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 font-semibold leading-tight">All Reports</span>
|
||||
<span class="flex shrink-0 items-center justify-center text-brand">
|
||||
<CheckIcon
|
||||
v-if="currentReporterOrProject.length === 0"
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
|
||||
<FloatingPanel button-class="!h-10 !shadow-none !text-contrast" :auto-focus="false">
|
||||
<BlendIcon class="size-5" /> Advanced filters
|
||||
<template #panel>
|
||||
@@ -67,6 +129,7 @@
|
||||
class="!w-full"
|
||||
:options="reportTargetFilterTypes"
|
||||
:placeholder="formatMessage(commonMessages.filterByLabel)"
|
||||
@select="goToPage(1)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex min-w-64 flex-col gap-3">
|
||||
@@ -77,6 +140,7 @@
|
||||
class="!w-full"
|
||||
:options="reportIssueFilterTypes"
|
||||
:placeholder="formatMessage(commonMessages.filterByLabel)"
|
||||
@select="goToPage(1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,6 +151,7 @@
|
||||
class="!w-full"
|
||||
:options="projectTypeFilterTypes"
|
||||
:placeholder="formatMessage(commonMessages.filterByLabel)"
|
||||
@select="goToPage(1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +160,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-between">
|
||||
<div>
|
||||
Showing
|
||||
{{ itemsPerPage * (currentPage - 1) + 1 }}
|
||||
–
|
||||
{{ itemsPerPage * (currentPage - 1) + Math.min(itemsPerPage, paginatedReports.length) }}
|
||||
of {{ sortedReports.length }} reports
|
||||
</div>
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
@@ -111,18 +187,30 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BlendIcon, ListFilterIcon, SearchIcon, SortAscIcon, SortDescIcon } from '@modrinth/assets'
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
BlendIcon,
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
LayersIcon,
|
||||
ListFilterIcon,
|
||||
SearchIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { ExtendedReport } from '@modrinth/moderation'
|
||||
import {
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
FloatingPanel,
|
||||
MultiSelect,
|
||||
type MultiSelectItem,
|
||||
Pagination,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Report } from '@modrinth/utils'
|
||||
import type { Report, User } from '@modrinth/utils'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import ReportCard from '~/components/ui/moderation/ModerationReportCard.vue'
|
||||
@@ -254,6 +342,64 @@ const reportIssueFilterTypes = computed<ComboboxOption<string>[]>(() => {
|
||||
return [...base, ...sortedTypes.map((type) => ({ value: type, label: type }))]
|
||||
})
|
||||
|
||||
type ReportedType<T> = T & { report_item_count: number }
|
||||
|
||||
const currentReporterOrProject = ref<string[]>([])
|
||||
const reporterOrProjectOptions = computed<MultiSelectItem<string>[]>(() => {
|
||||
if (!allReports.value) return []
|
||||
const options: MultiSelectItem<string>[] = []
|
||||
|
||||
const uniqueProjectIds: { [id: string]: ReportedType<Labrinth.Projects.v2.Project> } = {}
|
||||
const uniqueReporterIds: { [id: string]: ReportedType<User> } = {}
|
||||
|
||||
for (const report of filteredReports.value) {
|
||||
if (report.project)
|
||||
uniqueProjectIds[report.project.id] = {
|
||||
...report.project,
|
||||
report_item_count: (uniqueProjectIds[report.project.id]?.report_item_count || 0) + 1,
|
||||
}
|
||||
if (report.reporter_user)
|
||||
uniqueReporterIds[report.reporter_user.id] = {
|
||||
...report.reporter_user,
|
||||
report_item_count: (uniqueReporterIds[report.reporter_user.id]?.report_item_count || 0) + 1,
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(uniqueProjectIds).length !== 0) {
|
||||
options.push({ type: 'section-header', label: 'Projects' })
|
||||
Object.values(uniqueProjectIds)
|
||||
.sort((a, b) =>
|
||||
a.report_item_count === b.report_item_count
|
||||
? a.title.localeCompare(b.title)
|
||||
: b.report_item_count - a.report_item_count,
|
||||
)
|
||||
.forEach((project) => {
|
||||
options.push({
|
||||
value: `project/${project.id}`,
|
||||
label: `${project.title} (${project.report_item_count})`,
|
||||
icon: project.icon_url ? h('img', { src: project.icon_url }) : undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
options.push({ type: 'section-header', label: 'Reporters' })
|
||||
Object.values(uniqueReporterIds)
|
||||
.sort((a, b) =>
|
||||
a.report_item_count === b.report_item_count
|
||||
? a.username.localeCompare(b.username)
|
||||
: b.report_item_count - a.report_item_count,
|
||||
)
|
||||
.forEach((reporter) => {
|
||||
options.push({
|
||||
value: `reporter/${reporter.id}`,
|
||||
label: `${reporter.username} (${reporter.report_item_count})`,
|
||||
icon: reporter.avatar_url ? h('img', { src: reporter.avatar_url }) : undefined,
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 15
|
||||
const totalPages = computed(() => Math.ceil((sortedReports.value?.length || 0) / itemsPerPage))
|
||||
@@ -379,7 +525,19 @@ const filteredReports = computed(() => {
|
||||
})
|
||||
|
||||
const sortedReports = computed(() => {
|
||||
const filtered = [...filteredReports.value]
|
||||
const reporterOrProjectFilter = currentReporterOrProject.value
|
||||
const filtered =
|
||||
reporterOrProjectFilter.length === 0
|
||||
? [...filteredReports.value]
|
||||
: filteredReports.value.filter((report) => {
|
||||
const reporterOrProjectFilterLookup = new Set(reporterOrProjectFilter)
|
||||
const reporterValue = report.reporter_user ? `reporter/${report.reporter_user.id}` : null
|
||||
const projectValue = report.project ? `project/${report.project.id}` : null
|
||||
return (
|
||||
(reporterValue && reporterOrProjectFilterLookup.has(reporterValue)) ||
|
||||
(projectValue && reporterOrProjectFilterLookup.has(projectValue))
|
||||
)
|
||||
})
|
||||
|
||||
if (currentSortTypeSorting.value === 'oldest') {
|
||||
filtered.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime())
|
||||
|
||||
Reference in New Issue
Block a user