Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ const expandedClasses = reactive<Set<string>>(new Set())
const autoExpandedFileIds = reactive<Set<string>>(new Set())
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
const highlightedSourceCache = reactive<Map<string, { source: string; lines: string[] }>>(new Map())
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 10
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 2

interface ClassGroup {
key: string
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/components/ui/thread/ConversationThread.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
v-if="sortedMessages.length > 0"
:disabled="!replyBody || isLoading"
@click="
isApproved(project)
isApproved(project) && !isStaff(auth.user)
? openReplyModal()
: runBlockingAction('reply', () => sendReply())
"
Expand All @@ -169,7 +169,7 @@
v-else
:disabled="!replyBody || isLoading"
@click="
isApproved(project)
isApproved(project) && !isStaff(auth.user)
? openReplyModal()
: runBlockingAction('send', () => sendReply())
"
Expand Down
180 changes: 144 additions & 36 deletions apps/frontend/src/pages/moderation/reports/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,81 @@
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
</div>

<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
<div
class="flex flex-col items-stretch justify-end gap-2 sm:flex-row sm:items-center lg:flex-shrink-0"
>
<Combobox
v-model="currentFilterType"
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
:options="filterTypes"
v-model="currentMessageFilter"
class="!w-full flex-grow sm:!w-[200px] sm:flex-grow-0"
:options="messageFilterTypes"
:placeholder="formatMessage(commonMessages.filterByLabel)"
@select="goToPage(1)"
>
<template #selected>
<template #selected="{ label: messageLabel }">
<span class="flex flex-row gap-2 align-middle font-semibold">
<ListFilterIcon class="size-5 flex-shrink-0 text-secondary" />
<span class="truncate text-contrast"
>{{ currentFilterType }} ({{ filteredReports.length }})</span
>{{ messageLabel }} ({{ sortedReports.length }})</span
>
</span>
</template>
</Combobox>

<Combobox
v-model="currentSortType"
v-model="currentSortTypeSorting"
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
:options="sortTypes"
:placeholder="formatMessage(commonMessages.sortByLabel)"
@select="goToPage(1)"
>
<template #selected>
<template #selected="{ label: sortingLabel }">
<span class="flex flex-row gap-2 align-middle font-semibold">
<SortAscIcon
v-if="currentSortType === 'Oldest'"
v-if="currentSortTypeSorting === 'oldest'"
class="size-5 flex-shrink-0 text-secondary"
/>
<SortDescIcon v-else class="size-5 flex-shrink-0 text-secondary" />
<span class="truncate text-contrast">{{ currentSortType }}</span>
<span class="truncate text-contrast">{{ sortingLabel }}</span>
</span>
</template>
</Combobox>

<FloatingPanel button-class="!h-10 !shadow-none !text-contrast" :auto-focus="false">
<BlendIcon class="size-5" /> Advanced filters
<template #panel>
<div class="flex min-w-64 flex-col gap-3">
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-secondary">Report target</span>
<Combobox
v-model="currentReportTargetFilter"
class="!w-full"
:options="reportTargetFilterTypes"
:placeholder="formatMessage(commonMessages.filterByLabel)"
/>
</div>
<div class="flex min-w-64 flex-col gap-3">
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-secondary">Issue type</span>
<Combobox
v-model="currentReportIssueFilter"
class="!w-full"
:options="reportIssueFilterTypes"
:placeholder="formatMessage(commonMessages.filterByLabel)"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-secondary">Project type</span>
<Combobox
v-model="currentProjectTypeFilter"
class="!w-full"
:options="projectTypeFilterTypes"
:placeholder="formatMessage(commonMessages.filterByLabel)"
/>
</div>
</div>
</template>
</FloatingPanel>
</div>
</div>

Expand All @@ -72,12 +111,13 @@
</template>

<script setup lang="ts">
import { ListFilterIcon, SearchIcon, SortAscIcon, SortDescIcon } from '@modrinth/assets'
import { BlendIcon, ListFilterIcon, SearchIcon, SortAscIcon, SortDescIcon } from '@modrinth/assets'
import type { ExtendedReport } from '@modrinth/moderation'
import {
Combobox,
type ComboboxOption,
commonMessages,
FloatingPanel,
Pagination,
StyledInput,
useVIntl,
Expand All @@ -93,6 +133,7 @@ useHead({ title: 'Reports queue - Modrinth' })
const { formatMessage } = useVIntl()
const route = useRoute()
const router = useRouter()
const auth = await useAuth()

const { data: allReports } = await useLazyAsyncData('new-moderation-reports', async () => {
const startTime = performance.now()
Expand Down Expand Up @@ -168,22 +209,54 @@ watch(
},
)

const currentFilterType = ref('All')
const filterTypes: ComboboxOption<string>[] = [
{ value: 'All', label: 'All' },
{ value: 'Unread', label: 'Unread' },
{ value: 'Read', label: 'Read' },
const currentSortTypeSorting = ref('oldest')
const sortTypes: ComboboxOption<string>[] = [
{ value: 'oldest', label: 'Oldest' },
{ value: 'newest', label: 'Newest' },
]

const currentSortType = ref('Oldest')
const sortTypes: ComboboxOption<string>[] = [
{ value: 'Oldest', label: 'Oldest' },
{ value: 'Newest', label: 'Newest' },
const currentMessageFilter = ref('all')
const messageFilterTypes: ComboboxOption<string>[] = [
{ value: 'all', label: 'All' },
{ value: 'unread', label: 'Unread' },
{ value: 'read', label: 'Read' },
{ value: 'involved', label: 'Involved' },
]

const currentProjectTypeFilter = ref('all')
const projectTypeFilterTypes: ComboboxOption<string>[] = [
{ value: 'all', label: 'All project types' },
{ value: 'modpack', label: 'Modpacks' },
{ value: 'mod', label: 'Mods' },
{ value: 'resourcepack', label: 'Resource Packs' },
{ value: 'datapack', label: 'Data Packs' },
{ value: 'plugin', label: 'Plugins' },
{ value: 'shader', label: 'Shaders' },
{ value: 'minecraft_java_server', label: 'Servers' },
]

const currentReportTargetFilter = ref('all')
const reportTargetFilterTypes: ComboboxOption<string>[] = [
{ value: 'all', label: 'All' },
{ value: 'project', label: 'Projects' },
{ value: 'user', label: 'Users' },
{ value: 'version', label: 'Versions' },
]

const currentReportIssueFilter = ref('all')
const reportIssueFilterTypes = computed<ComboboxOption<string>[]>(() => {
const base: ComboboxOption<string>[] = [{ value: 'all', label: 'All' }]
if (!allReports.value) return base

const issueTypes = new Set(allReports.value.map((report) => report.report_type))

const sortedTypes = Array.from(issueTypes).sort()
return [...base, ...sortedTypes.map((type) => ({ value: type, label: type }))]
})

const currentPage = ref(1)
const itemsPerPage = 15
const totalPages = computed(() => Math.ceil((filteredReports.value?.length || 0) / itemsPerPage))
const totalPages = computed(() => Math.ceil((sortedReports.value?.length || 0) / itemsPerPage))

const fuse = computed(() => {
if (!allReports.value || allReports.value.length === 0) return null
Expand Down Expand Up @@ -247,33 +320,68 @@ const baseFiltered = computed(() => {
return query.value && searchResults.value ? searchResults.value : [...allReports.value]
})

const typeFiltered = computed(() => {
if (currentFilterType.value === 'All') return baseFiltered.value
const filteredReports = computed(() => {
const messageFilter = currentMessageFilter.value
const projectTypeFilter = currentProjectTypeFilter.value
const reportTargetFilter = currentReportTargetFilter.value
const reportIssueFilter = currentReportIssueFilter.value

if (
messageFilter === 'all' &&
projectTypeFilter === 'all' &&
reportTargetFilter === 'all' &&
reportIssueFilter === 'all'
) {
return baseFiltered.value
}

return baseFiltered.value.filter((report) => {
const messageFilterPredicate = (report: ExtendedReport) => {
const messages = report.thread?.messages || []

if (messages.length === 0) {
return currentFilterType.value === 'Unread'
}
if (messageFilter === 'all') return true
if (messages.length === 0) return messageFilter === 'Unread'
if (!messages[messages.length - 1].author_id) return false

const lastMessage = messages[messages.length - 1]
if (!lastMessage.author_id) return false
if (messageFilter === 'involved') {
const userId = (auth.value.user as any)?.id
return userId && messages.some((message) => message.author_id === userId)
}

const roleMap = memberRoleMap.value.get(report.id)
if (!roleMap) return false

const authorRole = roleMap.get(lastMessage.author_id)
const authorRole = roleMap.get(messages[messages.length - 1].author_id)
const isModeratorMessage = authorRole === 'moderator' || authorRole === 'admin'

return currentFilterType.value === 'Read' ? isModeratorMessage : !isModeratorMessage
return messageFilter === 'Read' ? isModeratorMessage : !isModeratorMessage
}

const projectTypeFilterPredicate = (report: ExtendedReport) => {
return projectTypeFilter === 'all' || report.project?.project_type === projectTypeFilter
}

const reportTargetFilterPredicate = (report: ExtendedReport) => {
return reportTargetFilter === 'all' || report.item_type === reportTargetFilter
}

const reportIssueFilterPredicate = (report: ExtendedReport) => {
return reportIssueFilter === 'all' || report.report_type === reportIssueFilter
}

return baseFiltered.value.filter((report) => {
return (
messageFilterPredicate(report) &&
projectTypeFilterPredicate(report) &&
reportTargetFilterPredicate(report) &&
reportIssueFilterPredicate(report)
)
})
})

const filteredReports = computed(() => {
const filtered = [...typeFiltered.value]
const sortedReports = computed(() => {
const filtered = [...filteredReports.value]

if (currentSortType.value === 'Oldest') {
if (currentSortTypeSorting.value === 'oldest') {
filtered.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime())
} else {
filtered.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime())
Expand All @@ -283,10 +391,10 @@ const filteredReports = computed(() => {
})

const paginatedReports = computed(() => {
if (!filteredReports.value) return []
if (!sortedReports.value) return []
const start = (currentPage.value - 1) * itemsPerPage
const end = start + itemsPerPage
return filteredReports.value.slice(start, end)
return sortedReports.value.slice(start, end)
})

function goToPage(page: number) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/base/Combobox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
class="h-5 w-5 shrink-0"
/>
<span class="min-w-0 truncate text-primary font-semibold leading-tight">
<slot name="selected">{{ triggerText }}</slot>
<slot name="selected" :label="triggerText">{{ triggerText }}</slot>
</span>
</div>
<div class="flex items-center gap-1">
Expand Down
10 changes: 7 additions & 3 deletions packages/ui/src/components/base/FloatingPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ const props = withDefaults(
disabled?: boolean
buttonClass?: string
panelClass?: string
autoFocus?: boolean
}>(),
{
placement: 'bottom-end',
distance: 8,
disabled: false,
autoFocus: true,
},
)

Expand Down Expand Up @@ -157,9 +159,11 @@ async function open() {
await updatePanelPosition()
startPositionTracking()

setTimeout(() => {
focusPanelContent()
}, 50)
if (props.autoFocus) {
setTimeout(() => {
focusPanelContent()
}, 50)
}
}

function close() {
Expand Down
Loading