rules affect tech review detail statuses

This commit is contained in:
aecsocket
2026-07-24 13:49:20 +01:00
parent 4b07ed720e
commit 8d82c228cf
17 changed files with 760 additions and 210 deletions
@@ -5,14 +5,8 @@
<p class="m-0 break-words font-semibold text-contrast">
{{ trace.project_name }}
</p>
<p class="m-0 mt-1 flex flex-wrap items-center gap-1 text-sm text-secondary">
<span class="break-all">{{ trace.version_number }}</span>
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
<span class="break-all">{{ decodeTracePath(trace.file_name) }}</span>
<template v-if="trace.jar">
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
<span class="break-all">{{ decodeTracePath(trace.jar) }}</span>
</template>
<p class="m-0 mt-1 text-sm text-secondary">
<IssueDetailPath :segments="[trace.version_number, trace.file_name, trace.jar]" />
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
@@ -33,9 +27,11 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ChevronRightIcon, ExternalIcon } from '@modrinth/assets'
import { ExternalIcon } from '@modrinth/assets'
import { Badge, ButtonStyled } from '@modrinth/ui'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const props = defineProps<{
trace: Labrinth.TechReview.Internal.GlobalIssueDetailTrace
}>()
@@ -46,12 +42,4 @@ const localTraceLink = computed(
props.trace.detail_id,
)}`,
)
function decodeTracePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
</script>
@@ -65,7 +65,7 @@
</p>
<p class="m-0 break-all text-secondary">
<span class="font-semibold text-contrast">Path</span>
{{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }}
<IssueDetailPath :segments="[getLatestLocalTrace(trace)?.file_path]" />
</p>
</div>
</div>
@@ -133,6 +133,7 @@ import {
} from '@modrinth/ui'
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
@@ -165,14 +166,6 @@ function getLatestLocalTrace(trace: Labrinth.TechReview.Internal.GlobalIssueDeta
return trace.local_traces.at(-1)
}
function decodeTracePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function getSeverityBadgeColor(
severity: Labrinth.TechReview.Internal.DelphiSeverity | undefined,
): string {
@@ -0,0 +1,74 @@
<template>
<span class="inline-flex min-w-0 flex-wrap items-center gap-1">
<template v-for="(segment, index) in decodedSegments" :key="`${segment}-${index}`">
<ChevronRightIcon v-if="index > 0" class="size-4 shrink-0" aria-hidden="true" />
<span
v-tooltip="isTruncated(segment) ? segment : undefined"
class="break-all"
:class="{
'font-semibold text-contrast': emphasizeLast && index === decodedSegments.length - 1,
'text-secondary': emphasizeLast && index < decodedSegments.length - 1,
}"
>
{{ formatSegment(segment) }}
</span>
</template>
</span>
</template>
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
const props = withDefaults(
defineProps<{
segments: readonly (string | null | undefined)[]
truncate?: boolean
maxLength?: number
emphasizeLast?: boolean
decode?: boolean
hideBaseMrpack?: boolean
}>(),
{
truncate: false,
maxLength: 120,
emphasizeLast: false,
decode: true,
hideBaseMrpack: false,
},
)
const decodedSegments = computed(() => {
const segments = props.segments
.flatMap((segment) => segment?.split('#') ?? [])
.filter((segment) => segment.length > 0)
.map((segment) => (props.decode ? decodePath(segment) : segment))
if (props.hideBaseMrpack && segments[0]?.toLowerCase().endsWith('.mrpack')) {
return segments.slice(1)
}
return segments
})
function decodePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function isTruncated(segment: string): boolean {
return props.truncate && segment.length > props.maxLength
}
function formatSegment(segment: string): string {
if (!isTruncated(segment)) return segment
const separator = '...'
const charsToShow = props.maxLength - separator.length
const frontChars = Math.ceil(charsToShow / 3)
const backChars = Math.floor((charsToShow * 2) / 3)
return segment.slice(0, frontChars) + separator + segment.slice(-backChars)
}
</script>
@@ -6,7 +6,6 @@ import {
CheckCheckIcon,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
ClipboardCopyIcon,
CodeIcon,
CopyIcon,
@@ -49,6 +48,7 @@ import {
import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
import type { UnsafeFile } from '~/components/ui/moderation/MaliciousSummaryModal.vue'
import ThreadView from '~/components/ui/thread/ThreadView.vue'
@@ -601,6 +601,29 @@ async function copyToClipboard(code: string, detailId: string) {
}
}
async function copyDetailCelInput(detailId: string) {
if (copyingCelDetails.has(detailId)) return
copyingCelDetails.add(detailId)
try {
const input = await client.labrinth.tech_review_internal.getDetailRuleInput(detailId)
await navigator.clipboard.writeText(JSON.stringify(input, null, 2))
copiedCelDetails.add(detailId)
setTimeout(() => {
copiedCelDetails.delete(detailId)
}, 2000)
} catch (error) {
console.error('Failed to copy CEL input:', error)
addNotification({
type: 'error',
title: 'Failed to copy CEL input',
text: 'An error occurred while loading the trace rule input.',
})
} finally {
copyingCelDetails.delete(detailId)
}
}
function getDetailDecision(
detailId: string,
backendStatus: Labrinth.TechReview.Internal.DelphiReportIssueStatus,
@@ -999,6 +1022,8 @@ async function updateGlobalDetailStatus(
const expandedClasses = reactive<Set<string>>(new Set())
const autoExpandedFileIds = reactive<Set<string>>(new Set())
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
const copyingCelDetails = reactive<Set<string>>(new Set())
const copiedCelDetails = reactive<Set<string>>(new Set())
const highlightedSourceCache = reactive<Map<string, { source: string; lines: string[] }>>(new Map())
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 2
@@ -1025,7 +1050,7 @@ interface JarGroup {
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
if (!jar) return []
const segments = jar
.split(/[/#]/)
.split('#')
.map((s) => decodeURIComponent(s.trim()))
.filter((s) => s.length > 0)
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
@@ -1770,27 +1795,12 @@ function copyId() {
class="border-b border-solid border-surface-1 px-4 py-3"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap items-center gap-1">
<template
v-for="(segment, index) in jarGroup.segments"
:key="`${jarGroup.key}-${index}`"
>
<span
class="font-mono text-sm"
:class="
index === jarGroup.segments.length - 1
? 'font-semibold text-contrast'
: 'text-secondary'
"
>
{{ segment }}
</span>
<ChevronRightIcon
v-if="index < jarGroup.segments.length - 1"
class="size-4 text-secondary"
/>
</template>
</div>
<IssueDetailPath
:segments="jarGroup.segments"
:decode="false"
class="font-mono text-sm"
emphasize-last
/>
<div
v-if="getJarRemainingUnmarkedCount(jarGroup) > 0"
@@ -1864,9 +1874,12 @@ function copyId() {
</button>
</ButtonStyled>
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
truncateMiddle(classItem.filePath)
}}</span>
<IssueDetailPath
:segments="[classItem.jar, classItem.filePath]"
class="font-mono font-semibold"
hide-base-mrpack
truncate
/>
<div
class="rounded-full border-solid px-2.5 py-1"
@@ -1941,6 +1954,23 @@ function copyId() {
</div>
<div class="detail-verdict-action-groups">
<ButtonStyled>
<button
type="button"
:disabled="copyingCelDetails.has(flag.detail.id)"
@click="copyDetailCelInput(flag.detail.id)"
>
<LoaderCircleIcon
v-if="copyingCelDetails.has(flag.detail.id)"
class="animate-spin"
aria-hidden="true"
/>
<ClipboardCopyIcon v-else aria-hidden="true" />
<span aria-live="polite">
{{ copiedCelDetails.has(flag.detail.id) ? 'Copied!' : 'Copy CEL' }}
</span>
</button>
</ButtonStyled>
<div
class="detail-verdict-buttons"
role="group"
@@ -43,7 +43,7 @@
</p>
<p class="m-0 break-all text-secondary">
<span class="font-semibold text-contrast">Path</span>
{{ decodeTracePath(latestLocalTrace.file_path) }}
<IssueDetailPath :segments="[latestLocalTrace.file_path]" />
</p>
</div>
</div>
@@ -103,6 +103,7 @@ import {
} from '@modrinth/ui'
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
@@ -137,14 +138,6 @@ const pageEnd = computed(() =>
)
const latestLocalTrace = computed(() => trace.value?.local_traces.at(-1))
function decodeTracePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
switch (severity) {
case 'severe':
@@ -327,11 +327,11 @@
<p
class="m-0 mt-0.5 flex min-w-0 items-center gap-1 font-mono text-xs text-secondary"
>
<template v-if="detail.jar">
<span class="truncate">{{ detail.jar }}</span>
<ChevronRightIcon class="size-3.5 shrink-0" aria-hidden="true" />
</template>
<span class="truncate">{{ detail.file_path }}</span>
<IssueDetailPath
:segments="[detail.jar, detail.file_path]"
hide-base-mrpack
truncate
/>
</p>
</div>
<ButtonStyled>
@@ -385,7 +385,6 @@
import { type Labrinth, SseParser } from '@modrinth/api-client'
import {
ArrowLeftIcon,
ChevronRightIcon,
EditIcon,
EyeOffIcon,
ExternalIcon,
@@ -409,6 +408,8 @@ import { useDebounceFn } from '@vueuse/core'
import type { Ace } from 'ace-builds'
import type { Component } from 'vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const DEFAULT_RULE = `input.trace.issue_type == "OBFUSCATED_NAMES"
? {"severity": "low", "hidden": false}
: null`