chore: improve moderation ux (#6035)

* feat: save project review queue filters

* reduce unnecessary network calls + prepr

* missed file

* ui tweaks

* add fucked up

* add label + prepr

* prepr

* update legacy badge labels

* globe

* fix margin

* be more reasonable

* pending state

* fix double review, prepr

* small badge text
This commit is contained in:
Prospector
2026-05-08 01:40:28 -07:00
committed by GitHub
parent 758ed818c8
commit 9c99518497
9 changed files with 475 additions and 233 deletions
@@ -1,12 +1,19 @@
<template>
<div class="shadow-card rounded-2xl border border-surface-5 bg-surface-3 p-4">
<div class="shadow-card rounded-2xl border border-solid border-surface-5 bg-surface-3 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<Avatar
:src="queueEntry.project.icon_url"
size="4rem"
class="rounded-2xl border border-surface-5 bg-surface-4 !shadow-none"
/>
<NuxtLink
:to="`/project/${queueEntry.project.slug}`"
target="_blank"
tabindex="-1"
class="flex"
>
<Avatar
:src="queueEntry.project.icon_url"
size="4rem"
class="rounded-2xl border border-surface-5 bg-surface-4 !shadow-none"
/>
</NuxtLink>
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<NuxtLink
@@ -22,11 +29,15 @@
<component
:is="getProjectTypeIcon(queueEntry.project.project_types[0] as any)"
aria-hidden="true"
class="h-4 w-4"
class="size-4"
/>
<span class="text-sm font-medium text-secondary">
{{
queueEntry.project.project_types.map((t) => formatProjectType(t, true)).join(', ')
queueEntry.project.project_types.length === 0
? '???'
: queueEntry.project.project_types
.map((t) => formatProjectType(t, true))
.join(', ')
}}
</span>
</div>
@@ -35,37 +46,39 @@
class="flex items-center gap-2 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
>
<span class="text-sm text-secondary">Requesting</span>
<Badge :type="queueEntry.project.requested_status" class="status" />
<Badge :type="queueEntry.project.requested_status" class="text-sm" />
</div>
</div>
<div v-if="queueEntry.owner" class="flex items-center gap-1">
<Avatar
:src="queueEntry.owner.user.avatar_url"
size="1.5rem"
circle
class="border border-surface-5 bg-surface-4 !shadow-none"
/>
<div v-if="queueEntry.ownership?.kind === 'user'">
<NuxtLink
:to="`/user/${queueEntry.owner.user.username}`"
:to="`/user/${queueEntry.ownership.id}`"
target="_blank"
class="text-sm font-medium text-secondary hover:underline"
class="flex w-fit min-w-40 items-center gap-1 text-sm font-medium text-secondary hover:underline"
>
{{ queueEntry.owner.user.username }}
<Avatar
:src="queueEntry.ownership.icon_url"
size="1.5rem"
circle
class="border border-surface-5 bg-surface-4 !shadow-none"
/>
{{ queueEntry.ownership.name }}
</NuxtLink>
</div>
<div v-else-if="queueEntry.org" class="flex items-center gap-1">
<div
v-else-if="queueEntry.ownership?.kind === 'organization'"
class="flex items-center gap-1"
>
<Avatar
:src="queueEntry.org.icon_url"
:src="queueEntry.ownership.icon_url"
size="1.5rem"
circle
class="border border-surface-5 bg-surface-4 !shadow-none"
/>
<NuxtLink
:to="`/organization/${queueEntry.org.slug}`"
:to="`/organization/${queueEntry.ownership.id}`"
target="_blank"
class="text-sm font-medium text-secondary hover:underline"
>
{{ queueEntry.org.name }}
{{ queueEntry.ownership.name }}
</NuxtLink>
</div>
</div>
@@ -84,25 +97,20 @@
</span>
<div class="flex items-center gap-2">
<ButtonStyled circular color="orange">
<button @click="openProjectForReview">
<ScaleIcon class="size-5" />
<ButtonStyled circular>
<button v-tooltip="'Copy ID'" @click="copyId">
<ClipboardCopyIcon />
</button>
</ButtonStyled>
<ButtonStyled circular>
<OverflowMenu :options="quickActions" :dropdown-id="`${baseId}-quick-actions`">
<template #default>
<EllipsisVerticalIcon class="size-4" />
</template>
<template #copy-id>
<ClipboardCopyIcon />
<span class="hidden sm:inline">Copy ID</span>
</template>
<template #copy-link>
<LinkIcon />
<span class="hidden sm:inline">Copy link</span>
</template>
</OverflowMenu>
<button v-tooltip="'Copy link'" @click="copyLink">
<LinkIcon />
</button>
</ButtonStyled>
<ButtonStyled v-tooltip="'Begin review'" circular color="orange">
<button @click="openProjectForReview">
<ScaleIcon />
</button>
</ButtonStyled>
</div>
</div>
@@ -111,15 +119,13 @@
</template>
<script setup lang="ts">
import { ClipboardCopyIcon, EllipsisVerticalIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import {
Avatar,
Badge,
ButtonStyled,
getProjectTypeIcon,
injectNotificationManager,
OverflowMenu,
type OverflowMenuOption,
useFormatDateTime,
useRelativeTime,
} from '@modrinth/ui'
@@ -143,8 +149,6 @@ const formatDateTimeFull = useFormatDateTime({
timeZone: 'UTC',
})
const baseId = useId()
const props = defineProps<{
queueEntry: ModerationProject
}>()
@@ -185,34 +189,27 @@ const formattedDate = computed(() => {
}
})
const quickActions: OverflowMenuOption[] = [
{
id: 'copy-link',
action: () => {
const base = window.location.origin
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
navigator.clipboard.writeText(projectUrl).then(() => {
addNotification({
type: 'success',
title: 'Project link copied',
text: 'The link to this project has been copied to your clipboard.',
})
})
},
},
{
id: 'copy-id',
action: () => {
navigator.clipboard.writeText(props.queueEntry.project.id).then(() => {
addNotification({
type: 'success',
title: 'Project ID copied',
text: 'The ID of this project has been copied to your clipboard.',
})
})
},
},
]
function copyLink() {
const base = window.location.origin
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
navigator.clipboard.writeText(projectUrl).then(() => {
addNotification({
type: 'success',
title: 'Project link copied',
text: 'The link to this project has been copied to your clipboard.',
})
})
}
function copyId() {
navigator.clipboard.writeText(props.queueEntry.project.id).then(() => {
addNotification({
type: 'success',
title: 'Project ID copied',
text: 'The ID of this project has been copied to your clipboard.',
})
})
}
function openProjectForReview() {
emit('startFromProject', props.queueEntry.project.id)