Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c36977b
feat: filtering + sorting alignment
IMB11 Jan 12, 2026
982c8a7
polish: malicious summary modal changes
IMB11 Jan 12, 2026
a2dac72
feat: better filter row using floating panel
IMB11 Jan 12, 2026
7e58491
fix: re-enable request
IMB11 Jan 12, 2026
78d0703
fix: lint
IMB11 Jan 12, 2026
8243c17
polish: jump back to files tab qol
IMB11 Jan 12, 2026
524bd1e
feat: scroll to top of next card when done
IMB11 Jan 12, 2026
42ac627
fix: show lock icon on preview msg
IMB11 Jan 12, 2026
7f1da80
feat: download no _blank
IMB11 Jan 12, 2026
94e652c
feat: show also marked in notif
IMB11 Jan 12, 2026
14d2ce9
feat: auto expand if only one class in the file
IMB11 Jan 12, 2026
a624583
feat: proper page titles
IMB11 Jan 12, 2026
7c6a6bf
fix: text-contrast typo
IMB11 Jan 12, 2026
dba5391
fix: lint
IMB11 Jan 12, 2026
4815655
Merge branch 'main' into cal/tech-review-qa-2
IMB11 Jan 12, 2026
494e5ee
Merge branch 'main' into cal/tech-review-qa-2
Prospector Jan 15, 2026
8ab2c6b
Merge branch 'main' into cal/tech-review-qa-2
Prospector Jan 16, 2026
755feef
Merge branch 'main' into cal/tech-review-qa-2
Prospector Jan 16, 2026
0829d9b
Merge branch 'main' into cal/tech-review-qa-2
IMB11 Jan 18, 2026
8583e78
feat: QA changes
IMB11 Jan 18, 2026
d1e12d6
feat: individual report page + more qa
IMB11 Jan 18, 2026
8f2d342
fix: back btn
IMB11 Jan 18, 2026
e90a5a5
fix: broken import
IMB11 Jan 18, 2026
73a643f
Merge branch 'main' into cal/tech-review-qa-2
IMB11 Jan 19, 2026
fb84ded
Merge branch 'main' into cal/tech-review-qa-2
IMB11 Jan 19, 2026
3acc7fc
feat: quick reply msgs
IMB11 Jan 19, 2026
6dd9fce
fix: in other queue filter
IMB11 Jan 19, 2026
147bc14
fix: caching threads wrongly
IMB11 Jan 19, 2026
0109680
fix: flag filter
IMB11 Jan 19, 2026
f7015a9
feat: toggle enabled by default
IMB11 Jan 20, 2026
cbc1a52
fix: dont make btns opacity 50
IMB11 Jan 20, 2026
33cd7d5
Merge branch 'main' into cal/tech-review-qa-2
IMB11 Jan 20, 2026
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
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ClipboardCopyIcon, LoaderCircleIcon, XIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, DownloadIcon, LoaderCircleIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, CopyCode, NewModal } from '@modrinth/ui'
import { ref, useTemplateRef } from 'vue'

Expand Down Expand Up @@ -38,15 +38,16 @@ async function fetchVersionHashes(versionIds: string[]) {
// TODO: switch to api-client once truman's vers stuff is merged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could refactor to use the api client if u feel like it

const version = (await useBaseFetch(`version/${versionId}`)) as {
files: Array<{
id?: string
filename: string
file_name?: string
hashes: { sha512: string; sha1: string }
}>
}
const filesMap = new Map<string, string>()
for (const file of version.files) {
const name = file.file_name ?? file.filename
filesMap.set(name, file.hashes.sha512)
if (file.id) {
filesMap.set(file.id, file.hashes.sha512)
}
}
versionDataCache.value.set(versionId, { files: filesMap, loading: false })
} catch (error) {
Expand All @@ -60,8 +61,8 @@ async function fetchVersionHashes(versionIds: string[]) {
}
}

function getFileHash(versionId: string, fileName: string): string | undefined {
return versionDataCache.value.get(versionId)?.files.get(fileName)
function getFileHash(versionId: string, fileId: string): string | undefined {
return versionDataCache.value.get(versionId)?.files.get(fileId)
}

function isHashLoading(versionId: string): boolean {
Expand Down Expand Up @@ -114,6 +115,7 @@ defineExpose({ show, hide })
<th class="pb-2">Version ID</th>
<th class="pb-2">File Name</th>
<th class="pb-2">CDN Link</th>
<th class="pb-2">Download</th>
</tr>
</thead>
<tbody>
Expand All @@ -124,11 +126,11 @@ defineExpose({ show, hide })
class="size-4 animate-spin text-secondary"
/>
<ButtonStyled
v-else-if="getFileHash(item.file.version_id, item.file.file_name)"
v-else-if="getFileHash(item.file.version_id, item.file.file_id)"
size="small"
type="standard"
>
<button @click="copy(getFileHash(item.file.version_id, item.file.file_name)!)">
<button @click="copy(getFileHash(item.file.version_id, item.file.file_id)!)">
<ClipboardCopyIcon class="size-4" />
Copy
</button>
Expand All @@ -141,14 +143,21 @@ defineExpose({ show, hide })
<td class="py-1 pr-2">
<CopyCode :text="item.file.file_name" />
</td>
<td class="py-1">
<td class="py-1 pr-2">
<ButtonStyled size="small" type="standard">
<button @click="copy(item.file.download_url)">
<ClipboardCopyIcon class="size-4" />
Copy
</button>
</ButtonStyled>
</td>
<td class="py-1">
<ButtonStyled circular size="small">
<a :href="item.file.download_url" :download="item.file.file_name" target="_blank">
<DownloadIcon />
</a>
</ButtonStyled>
</td>
</tr>
</tbody>
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
LinkIcon,
LoaderCircleIcon,
ShieldCheckIcon,
TimerIcon,
} from '@modrinth/assets'
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
import {
Expand Down Expand Up @@ -113,8 +114,8 @@ const quickActions = computed<OverflowMenuOption[]>(() => {
navigator.clipboard.writeText(props.item.project.id).then(() => {
addNotification({
type: 'success',
title: 'Technical Report ID copied',
text: 'The ID of this report has been copied to your clipboard.',
title: 'Project ID copied',
text: 'The ID of this project has been copied to your clipboard.',
})
})
},
Expand Down Expand Up @@ -265,6 +266,13 @@ const severityColor = computed(() => {
}
})

const isProjectApproved = computed(() => {
const status = props.item.project.status
return (
status === 'approved' || status === 'archived' || status === 'unlisted' || status === 'private'
)
})

const formattedDate = computed(() => {
const dates = props.item.reports.map((r) => new Date(r.created))
const earliest = new Date(Math.min(...dates.map((d) => d.getTime())))
Expand Down Expand Up @@ -369,12 +377,16 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
if (detailKey) break
}

let otherMatchedCount = 0
if (detailKey) {
for (const report of props.item.reports) {
for (const issue of report.issues) {
for (const detail of issue.details) {
if (detail.key === detailKey) {
detailDecisions.value.set(detail.id, decision)
if (detail.id !== detailId) {
otherMatchedCount++
}
}
}
}
Expand All @@ -391,17 +403,31 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
}
}

// Jump back to Files tab when all flags in the current file are marked
if (selectedFile.value) {
const markedCount = getFileMarkedCount(selectedFile.value)
const totalCount = getFileDetailCount(selectedFile.value)
if (markedCount === totalCount) {
backToFileList()
}
}

const otherText =
otherMatchedCount > 0
? ` (${otherMatchedCount} other trace${otherMatchedCount === 1 ? '' : 's'} also marked)`
: ''

if (verdict === 'safe') {
addNotification({
type: 'success',
title: 'Issue marked as pass',
text: 'This issue has been marked as a false positive.',
text: `This issue has been marked as a false positive.${otherText}`,
})
} else {
addNotification({
type: 'success',
title: 'Issue marked as fail',
text: 'This issue has been flagged as malicious.',
text: `This issue has been flagged as malicious.${otherText}`,
})
}
} catch (error) {
Expand Down Expand Up @@ -472,6 +498,17 @@ const groupedByClass = computed<ClassGroup[]>(() => {
})
})

// Auto-expand if there's only one class in the file
watch(
groupedByClass,
(classes) => {
if (classes.length === 1) {
expandedClasses.value.add(classes[0].filePath)
}
},
{ immediate: true },
)

function getHighestSeverityInClass(
flags: ClassGroup['flags'],
): Labrinth.TechReview.Internal.DelphiSeverity {
Expand Down Expand Up @@ -623,7 +660,7 @@ const threadWithPreview = computed(() => {
body: {
type: 'text',
body: reviewSummaryPreview.value,
private: false,
private: true,
replying_to: null,
associated_images: [],
},
Expand Down Expand Up @@ -747,9 +784,21 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
</div>

<div
class="rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
class="flex items-center gap-1 rounded-full border border-solid px-2.5 py-1"
:class="
isProjectApproved
? 'border-green bg-highlight-green'
: 'border-orange bg-highlight-orange'
"
>
<span class="text-sm font-medium text-secondary">Auto-Flagged</span>
<CheckIcon v-if="isProjectApproved" aria-hidden="true" class="h-4 w-4 text-green" />
<TimerIcon v-else aria-hidden="true" class="h-4 w-4 text-orange" />
<span
class="text-sm font-medium"
:class="isProjectApproved ? 'text-green' : 'text-orange'"
>
{{ isProjectApproved ? 'Live' : 'In review' }}
</span>
</div>

<div class="rounded-full px-2.5 py-1" :class="severityColor">
Expand Down Expand Up @@ -929,8 +978,6 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
:href="file.download_url"
:title="`Download ${file.file_name}`"
:download="file.file_name"
target="_blank"
rel="noopener noreferrer"
class="!border-px !border-surface-4"
tabindex="0"
>
Expand Down Expand Up @@ -1008,15 +1055,21 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
v-for="flag in classItem.flags"
:key="`${flag.issueId}-${flag.detail.id}`"
class="grid grid-cols-[1fr_auto_auto] items-center rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
:class="{
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
}"
>
<span class="text-base font-semibold text-contrast">{{
flag.issueType.replace(/_/g, ' ')
}}</span>
<span
class="text-base font-semibold text-contrast"
:class="{
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
}"
>{{ flag.issueType.replace(/_/g, ' ') }}</span
>

<div class="flex w-20 justify-center">
<div
class="flex w-20 justify-center"
:class="{
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
}"
>
<div
class="rounded-full border-solid px-2.5 py-1"
:class="getSeverityBadgeColor(flag.detail.severity)"
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/pages/moderation/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ import ModerationQueueCard from '~/components/ui/moderation/ModerationQueueCard.
import { enrichProjectBatch, type ModerationProject } from '~/helpers/moderation.ts'
import { useModerationStore } from '~/store/moderation.ts'

useHead({ title: 'Projects queue - Modrinth' })

const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const moderationStore = useModerationStore()
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/pages/moderation/reports/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ import Fuse from 'fuse.js'
import ReportCard from '~/components/ui/moderation/ModerationReportCard.vue'
import { enrichReportBatch } from '~/helpers/moderation.ts'

useHead({ title: 'Reports queue - Modrinth' })

const { formatMessage } = useVIntl()
const route = useRoute()
const router = useRouter()
Expand Down
Loading