mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
rules affect tech review detail statuses
This commit is contained in:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user