feat: requested moderation and tech review changes (#7019)

* feat: requested moderation and tech review changes

* chore: run prepr

* chore: run prepr

---------

Signed-off-by: ThatGravyBoat <gravy@thatgravyboat.tech>
This commit is contained in:
ThatGravyBoat
2026-08-07 13:34:03 -07:00
committed by GitHub
parent b0394de164
commit 5ef999bf8a
10 changed files with 223 additions and 20 deletions
@@ -856,6 +856,10 @@ async function updateDetailStatus(
updatingDetails.add(detailId)
const previousMarkedCount = selectedFile.value
? getFileMarkedCount(selectedFile.value)
: undefined
try {
await updateIssueDetails([{ detail_id: detailId, verdict }])
@@ -880,7 +884,7 @@ async function updateDetailStatus(
if (verdict !== 'pending' && selectedFile.value) {
const markedCount = getFileMarkedCount(selectedFile.value)
const totalCount = getFileDetailCount(selectedFile.value)
if (markedCount === totalCount) {
if (previousMarkedCount != markedCount && markedCount === totalCount) {
backToFileList()
}
}
@@ -938,6 +942,10 @@ async function updateGlobalDetailStatus(
updatingGlobalDetailKeys.add(detail.key)
const previousMarkedCount = selectedFile.value
? getFileMarkedCount(selectedFile.value)
: undefined
try {
await updateGlobalIssueDetail(detail.key, verdict)
@@ -958,7 +966,7 @@ async function updateGlobalDetailStatus(
if (verdict !== 'pending' && selectedFile.value) {
const markedCount = getFileMarkedCount(selectedFile.value)
const totalCount = getFileDetailCount(selectedFile.value)
if (markedCount === totalCount) {
if (previousMarkedCount != markedCount && markedCount === totalCount) {
backToFileList()
}
}
@@ -1355,8 +1363,10 @@ const canSubmitReview = computed(() => {
if (totalIssues === 0) return true
return allIssuesResolved.value
})
const hasSubmittedPassReview = ref(false)
async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
hasSubmittedPassReview.value = verdict === 'safe'
const editorContent = threadViewRef.value?.getReplyContent() || ''
let message: string | undefined
@@ -1523,13 +1533,39 @@ function copyId() {
<div class="h-px w-full bg-surface-5"></div>
<NavTabs
mode="local"
:links="navTabsLinks"
:active-index="activeTabIndex"
class="bg-surface-3! shadow-none!"
@tab-click="handleTabClick"
/>
<div class="flex flex-row justify-between">
<NavTabs
mode="local"
:links="navTabsLinks"
:active-index="activeTabIndex"
class="bg-surface-3! shadow-none!"
@tab-click="handleTabClick"
/>
<div v-if="selectedFile" class="flex flex-row items-end gap-2">
<ButtonLink
v-tooltip="`Download`"
type="outlined"
target="_blank"
:href="selectedFile.download_url"
:download="selectedFile.file_name"
class="!bg-surface-2"
aria-label="Download"
>
<DownloadIcon aria-hidden="true" />
</ButtonLink>
<ButtonLink
v-tooltip="`Open in Slicer`"
type="outlined"
target="_blank"
:href="`https://slicer.run/?url=${encodeURIComponent(selectedFile.download_url)}`"
class="!bg-surface-2"
aria-label="Open in Slicer"
>
<ExternalIcon aria-hidden="true" /> Open
</ButtonLink>
</div>
</div>
</div>
<div class="border-t border-surface-3 bg-surface-2">
@@ -1552,10 +1588,16 @@ function copyId() {
>
<template #additionalActions>
<Button
v-tooltip="!canSubmitReview ? 'There are still pending flags!' : undefined"
v-tooltip="
!canSubmitReview
? 'There are still pending flags!'
: hasSubmittedPassReview
? 'Project already passed!'
: undefined
"
type="colored"
color="brand"
:disabled="!canSubmitReview"
:disabled="!canSubmitReview || hasSubmittedPassReview"
@click="handleSubmitReview('safe')"
>
<ShieldCheckIcon /> Pass
@@ -1683,7 +1725,16 @@ function copyId() {
:download="file.file_name"
tabindex="0"
>
<DownloadIcon /> Download
<DownloadIcon />
</ButtonLink>
<ButtonLink
v-tooltip="`Open in Slicer`"
type="outlined"
target="_blank"
:href="`https://slicer.run/?url=${encodeURIComponent(file.download_url)}`"
aria-label="Open in Slicer"
>
<ExternalIcon aria-hidden="true" /> Open
</ButtonLink>
</div>
</div>
@@ -2063,7 +2114,7 @@ function copyId() {
v-tooltip="`Copy code`"
type="quiet"
:label="`Copy code`"
class="absolute right-2 top-2 border-[1px]"
class="!absolute right-2 top-2 border-[1px]"
@click="copyToClipboard(getClassDecompiledSource(classItem)!, classItem.key)"
>
<CopyIcon v-if="!showCopyFeedback.get(classItem.key)" />
@@ -2,9 +2,13 @@
<div>
<span class="flex flex-row items-center gap-2 text-sm text-secondary">
<GlobeIcon
v-if="props.global"
v-if="props.scope === 'project'"
v-tooltip="'Can be used without the checklist open if setting enabled.'"
/>
<ShieldCheckIcon
v-if="props.scope === 'tech-review'"
v-tooltip="'Used within the tech review pages'"
/>
{{ props.title }}
<IconButton
type="quiet"
@@ -45,14 +49,14 @@
</template>
<script setup lang="ts">
import { GlobeIcon, RotateCounterClockwiseIcon } from '@modrinth/assets'
import { GlobeIcon, RotateCounterClockwiseIcon, ShieldCheckIcon } from '@modrinth/assets'
import { type KeybindDefinition, toKeybindDefinition } from '@modrinth/moderation'
import { IconButton } from '@modrinth/ui'
import { onUnmounted } from 'vue'
const props = defineProps<{
title: string
global: boolean
scope: string
definitions: KeybindDefinition[]
default: KeybindDefinition[]
onChange: (definitions: KeybindDefinition[]) => void
@@ -12,7 +12,7 @@ const keybinds = useModerationKeybinds()
v-for="[id, keybind] in keybinds"
:key="id"
:title="keybind.description"
:global="keybind.scope === 'project'"
:scope="keybind.scope"
:definitions="keybind.keybind"
:default="keybind.defaultKeybind"
:on-change="
@@ -144,6 +144,22 @@
<DownloadIcon aria-hidden="true" />
{{ formatMessage(commonMessages.downloadButton) }}
</ButtonLink>
<ButtonLink
v-if="
!!primaryFile?.url &&
isStaff(auth.user) &&
modSettings.get(moderationSettings.General.SlicerButtonInVersions)
"
v-tooltip="`Open in Slicer`"
type="quiet"
target="_blank"
:href="`https://slicer.run/?url=${encodeURIComponent(primaryFile?.url)}`"
class="!bg-button-bg"
aria-label="Open in Slicer"
>
<ExternalIcon aria-hidden="true" />
Open
</ButtonLink>
<ButtonLink
v-for="file in promotedFiles.filter(
(x) =>
@@ -502,6 +518,7 @@ import {
TrashIcon,
XIcon,
} from '@modrinth/assets'
import { moderationSettings } from '@modrinth/moderation'
import {
Admonition,
Button,
@@ -545,6 +562,7 @@ const emit = defineEmits<{
const data = useNuxtApp()
const route = useNativeRoute()
const router = useRouter()
const modSettings = useModerationSettings()
const auth = await useAuth()
const tags = useGeneratedState()
const client = injectModrinthClient()
@@ -53,6 +53,21 @@
>
<DownloadIcon aria-hidden="true" />
</ButtonLink>
<ButtonLink
v-if="
!!getPrimaryFile(version) &&
isStaff(auth.user) &&
modSettings.get(moderationSettings.General.SlicerButtonInVersions)
"
v-tooltip="`Open in Slicer`"
type="quiet"
target="_blank"
:href="`https://slicer.run/?url=${encodeURIComponent(createDownloadUrl(version))}`"
class="!w-9 !rounded-full !px-0 hover:!bg-button-bg"
aria-label="Open in Slicer"
>
<ExternalIcon aria-hidden="true" />
</ButtonLink>
<TeleportOverflowMenu
v-if="currentMember"
type="quiet"
@@ -268,6 +283,7 @@ import {
SpinnerIcon,
TrashIcon,
} from '@modrinth/assets'
import { moderationSettings } from '@modrinth/moderation'
import {
ButtonLink,
ConfirmModal,
@@ -277,6 +293,7 @@ import {
ProjectPageVersions,
TeleportOverflowMenu,
} from '@modrinth/ui'
import { isStaff } from '@modrinth/utils'
import { onMounted, useTemplateRef, watch } from 'vue'
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
@@ -289,6 +306,7 @@ const { createProjectDownloadUrl, updateVersionsFilterContext } = useCdnDownload
const tags = useGeneratedState()
const flags = useFeatureFlags()
const modSettings = useModerationSettings()
const auth = await useAuth()
const client = injectModrinthClient()
@@ -12,6 +12,7 @@ import ModerationTechRevCard from '~/components/ui/moderation/ModerationTechRevC
const client = injectModrinthClient()
const queryClient = useQueryClient()
const route = useRoute()
const keybinds = useModerationKeybinds()
const projectId = String(useRouteId('project'))
@@ -269,6 +270,34 @@ function handleShowMaliciousSummary(unsafeFiles: UnsafeFile[]) {
function refetch() {
queryClient.invalidateQueries({ queryKey: ['tech-review-project-report', projectId] })
}
function handleKeybinds(event: KeyboardEvent) {
keybinds.value.handle(event, {
scope: 'tech-review',
actions: {
goToTop: () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
},
goToBottom: () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth',
})
},
},
})
}
onMounted(() => {
window.addEventListener('keydown', handleKeybinds)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeybinds)
})
</script>
<template>
@@ -32,6 +32,7 @@ useHead({ title: 'Tech review queue - Modrinth' })
const client = injectModrinthClient()
const queryClient = useQueryClient()
const keybinds = useModerationKeybinds()
const currentPage = ref(1)
const API_PAGE_SIZE = 50
@@ -571,6 +572,50 @@ watch(totalPages, (pages) => {
// complete: 20,
// }
// })
const CARD_BOTTOM_OFFSET = 210
function handleKeybinds(event: KeyboardEvent) {
keybinds.value.handle(event, {
scope: 'tech-review',
actions: {
goToTop: () => {
Array.from(cardRefs.values())
.filter((card) => card.getBoundingClientRect().top <= 0)
.reduce((prev, curr) =>
curr.getBoundingClientRect().top > prev.getBoundingClientRect().top ? curr : prev,
)
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
},
goToBottom: () => {
const nearestCard = Array.from(cardRefs.values())
.filter((card) => card.getBoundingClientRect().bottom >= window.innerHeight)
.reduce((prev, curr) =>
curr.getBoundingClientRect().top < prev.getBoundingClientRect().top ? curr : prev,
)
if (nearestCard) {
window.scrollTo({
behavior: 'smooth',
top:
nearestCard.getBoundingClientRect().bottom +
window.scrollY -
window.innerHeight +
CARD_BOTTOM_OFFSET,
})
}
},
},
})
}
onMounted(() => {
window.addEventListener('keydown', handleKeybinds)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeybinds)
})
</script>
<template>