mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -115,6 +115,18 @@ const keybinds: { [id: string]: KeybindListener } = {
|
||||
scope: 'checklist',
|
||||
action: (ctx) => ctx.actions.tryReject(),
|
||||
},
|
||||
'tech-review-top': {
|
||||
keybind: 'ArrowUp',
|
||||
description: 'Go to top of the tech review card',
|
||||
scope: 'tech-review',
|
||||
action: (ctx) => ctx.actions.goToTop(),
|
||||
},
|
||||
'tech-review-bottom': {
|
||||
keybind: 'ArrowDown',
|
||||
description: 'Go to bottom of the tech review card',
|
||||
scope: 'tech-review',
|
||||
action: (ctx) => ctx.actions.goToBottom(),
|
||||
},
|
||||
}
|
||||
|
||||
export default keybinds
|
||||
|
||||
@@ -27,6 +27,13 @@ const settings = {
|
||||
description: 'Whether private messages should be highlighted in the chat.',
|
||||
default: true,
|
||||
}),
|
||||
SlicerButtonInVersions: setting.asToggle({
|
||||
type: 'toggle',
|
||||
id: 'slicer-button-in-versions',
|
||||
title: 'Show Slicer Button in Versions Table and Page',
|
||||
description: 'Whether the slicer button should be shown in the versions table and page.',
|
||||
default: false,
|
||||
}),
|
||||
AdjustPageAlignment: setting.asEnum({
|
||||
type: 'enum',
|
||||
id: 'adjust-page-alignment',
|
||||
|
||||
@@ -16,6 +16,11 @@ export interface ModerationActions {
|
||||
tryEditMessage: () => void
|
||||
}
|
||||
|
||||
export interface TechReviewActions {
|
||||
goToTop: () => void
|
||||
goToBottom: () => void
|
||||
}
|
||||
|
||||
export interface ModerationState {
|
||||
currentStage: number
|
||||
totalStages: number
|
||||
@@ -44,7 +49,15 @@ export type ModerationChecklistContext = {
|
||||
actions: ModerationActions
|
||||
}
|
||||
|
||||
export type ModerationContext = ModerationProjectContext | ModerationChecklistContext
|
||||
export type ModerationTechReviewContext = {
|
||||
scope: 'tech-review'
|
||||
actions: TechReviewActions
|
||||
}
|
||||
|
||||
export type ModerationContext =
|
||||
| ModerationProjectContext
|
||||
| ModerationChecklistContext
|
||||
| ModerationTechReviewContext
|
||||
|
||||
export interface KeybindDefinition {
|
||||
key: string
|
||||
@@ -58,7 +71,7 @@ export interface KeybindDefinition {
|
||||
export type BaseKeybindListener<T> = {
|
||||
keybind: KeybindDefinition | KeybindDefinition[] | string | string[]
|
||||
description: string
|
||||
scope: 'project' | 'checklist'
|
||||
scope: 'project' | 'checklist' | 'tech-review'
|
||||
enabled?: (ctx: T) => boolean
|
||||
action: (ctx: T) => void
|
||||
}
|
||||
@@ -69,7 +82,13 @@ export type KeybindProjectListener = BaseKeybindListener<ModerationProjectContex
|
||||
export type KeybindChecklistListener = BaseKeybindListener<ModerationChecklistContext> & {
|
||||
scope: 'checklist'
|
||||
}
|
||||
export type KeybindListener = KeybindProjectListener | KeybindChecklistListener
|
||||
export type KeybindTechReviewListener = BaseKeybindListener<ModerationTechReviewContext> & {
|
||||
scope: 'tech-review'
|
||||
}
|
||||
export type KeybindListener =
|
||||
| KeybindProjectListener
|
||||
| KeybindChecklistListener
|
||||
| KeybindTechReviewListener
|
||||
|
||||
export function parseKeybind(keybindString: string): KeybindDefinition {
|
||||
const parts = keybindString.split('+').map((p) => p.trim().toLowerCase())
|
||||
|
||||
Reference in New Issue
Block a user