mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: tech review global trace database (#6648)
* global detail verdicts system * global traces frontend * split global routes into own mod * tweak migration to only do no-key check at app level * prepr
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="rounded-lg border border-divider bg-bg-raised p-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 break-words font-semibold text-contrast">
|
||||
{{ trace.project_name }}
|
||||
</p>
|
||||
<p class="m-0 mt-1 break-all text-sm text-secondary">
|
||||
Project {{ trace.project_slug ?? trace.project_id }} / Version
|
||||
{{ trace.version_number }} / File {{ trace.file_name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm text-secondary">Local</span>
|
||||
<Badge :type="trace.local_status" />
|
||||
<span class="text-sm text-secondary">Effective</span>
|
||||
<Badge :type="trace.effective_status" />
|
||||
<ButtonStyled>
|
||||
<NuxtLink :to="localTraceLink">
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
View
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid gap-2 text-sm text-secondary md:grid-cols-2">
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Issue</span>
|
||||
{{ trace.issue_type }}
|
||||
</p>
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Severity</span>
|
||||
{{ trace.severity }}
|
||||
</p>
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Path</span>
|
||||
{{ trace.file_path }}
|
||||
</p>
|
||||
<p v-if="trace.jar" class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">JAR</span>
|
||||
{{ trace.jar }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
trace: Labrinth.TechReview.Internal.GlobalIssueDetailTrace
|
||||
}>()
|
||||
|
||||
const localTraceLink = computed(
|
||||
() =>
|
||||
`/moderation/technical-review/${props.trace.project_id}?detail=${encodeURIComponent(
|
||||
props.trace.detail_id,
|
||||
)}`,
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div>
|
||||
<form class="flex flex-col gap-2 sm:flex-row" @submit.prevent="executeSearch">
|
||||
<StyledInput
|
||||
v-model="query"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Search global trace keys..."
|
||||
clearable
|
||||
wrapper-class="flex-1 w-full"
|
||||
/>
|
||||
<ButtonStyled color="brand">
|
||||
<button type="submit" :disabled="isLoading">
|
||||
<SearchIcon aria-hidden="true" />
|
||||
Search
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && !loadError && total > 0"
|
||||
class="mt-4 flex flex-wrap items-center justify-between gap-3"
|
||||
>
|
||||
<p class="m-0 text-sm text-secondary">Showing {{ pageStart }}-{{ pageEnd }} of {{ total }}</p>
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-if="isLoading"
|
||||
type="no-search-result"
|
||||
heading="Loading global detail traces..."
|
||||
/>
|
||||
<EmptyState
|
||||
v-else-if="loadError"
|
||||
type="no-search-result"
|
||||
heading="Failed to load global detail traces"
|
||||
/>
|
||||
<div v-else-if="traces.length > 0" class="mt-4 flex flex-col gap-3">
|
||||
<article
|
||||
v-for="trace in traces"
|
||||
:key="trace.detail_key"
|
||||
class="universal-card flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
||||
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
||||
Trace
|
||||
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="m-0 mt-1 text-sm text-secondary">
|
||||
{{ formatTraceCount(trace.local_trace_count) }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge :type="trace.verdict" />
|
||||
</div>
|
||||
|
||||
<div v-if="getPreviewLocalTraces(trace).length > 0" class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="getVisibleLocalTraceTotal(trace) > getPreviewLocalTraces(trace).length"
|
||||
class="flex flex-wrap items-center justify-between gap-2"
|
||||
>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Showing first {{ getPreviewLocalTraces(trace).length }} of
|
||||
{{ getVisibleLocalTraceTotal(trace) }} local traces
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<NuxtLink :to="getGlobalTraceLink(trace)">
|
||||
<ListIcon aria-hidden="true" />
|
||||
View all
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<GlobalDetailLocalTraceCard
|
||||
v-for="localTrace in getPreviewLocalTraces(trace)"
|
||||
:key="localTrace.detail_id"
|
||||
:trace="localTrace"
|
||||
/>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
type="no-search-result"
|
||||
heading="No local traces currently match this key"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
<EmptyState v-else type="no-search-result" heading="No global detail traces found" />
|
||||
|
||||
<div v-if="!isLoading && !loadError && total > 0" class="mt-4 flex justify-end">
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { HashIcon, ListIcon, SearchIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Badge,
|
||||
ButtonStyled,
|
||||
EmptyState,
|
||||
injectModrinthClient,
|
||||
Pagination,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const query = ref('')
|
||||
const activeQuery = ref<string | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const loadError = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 20
|
||||
const localTracePreviewLimit = 10
|
||||
const total = ref(0)
|
||||
const traces = ref<Labrinth.TechReview.Internal.GlobalIssueDetail[]>([])
|
||||
|
||||
const pageCount = computed(() => Math.max(Math.ceil(total.value / itemsPerPage), 1))
|
||||
const pageStart = computed(() =>
|
||||
total.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage + 1,
|
||||
)
|
||||
const pageEnd = computed(() => Math.min(currentPage.value * itemsPerPage, total.value))
|
||||
|
||||
function formatTraceCount(count: number) {
|
||||
return `${count} local ${count === 1 ? 'trace' : 'traces'}`
|
||||
}
|
||||
|
||||
function getPreviewLocalTraces(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return trace.local_traces.slice(0, localTracePreviewLimit)
|
||||
}
|
||||
|
||||
function getVisibleLocalTraceTotal(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return Math.max(trace.local_trace_count, trace.local_traces.length)
|
||||
}
|
||||
|
||||
function getGlobalTraceLink(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return `/moderation/global-traces/${encodeURIComponent(trace.detail_key)}`
|
||||
}
|
||||
|
||||
async function loadTraces() {
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
|
||||
try {
|
||||
const response = await client.labrinth.tech_review_internal.searchGlobalIssueDetails({
|
||||
query: activeQuery.value,
|
||||
limit: itemsPerPage,
|
||||
page: currentPage.value - 1,
|
||||
})
|
||||
|
||||
traces.value = response.traces
|
||||
total.value = response.total
|
||||
} catch (error) {
|
||||
console.error('Failed to load global detail traces', error)
|
||||
traces.value = []
|
||||
total.value = 0
|
||||
loadError.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function executeSearch() {
|
||||
activeQuery.value = query.value.trim() || null
|
||||
currentPage.value = 1
|
||||
await loadTraces()
|
||||
}
|
||||
|
||||
async function switchPage(page: number) {
|
||||
currentPage.value = page
|
||||
await loadTraces()
|
||||
}
|
||||
|
||||
onMounted(loadTraces)
|
||||
</script>
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
type User,
|
||||
} from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
|
||||
import type { UnsafeFile } from '~/components/ui/moderation/MaliciousSummaryModal.vue'
|
||||
import ThreadView from '~/components/ui/thread/ThreadView.vue'
|
||||
@@ -89,6 +89,7 @@ const props = defineProps<{
|
||||
thread: Labrinth.TechReview.Internal.Thread
|
||||
reports: FlattenedFileReport[]
|
||||
}
|
||||
focusedDetailId?: string | null
|
||||
loadingIssues: Set<string>
|
||||
decompiledSources: Map<string, string>
|
||||
}>()
|
||||
@@ -192,7 +193,12 @@ watch(selectedFile, (newFile) => {
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
async function updateIssueDetails(data: { detail_id: string; verdict: 'safe' | 'unsafe' }[]) {
|
||||
async function updateIssueDetails(
|
||||
data: {
|
||||
detail_id: string
|
||||
verdict: Labrinth.TechReview.Internal.DelphiReportIssueStatus
|
||||
}[],
|
||||
) {
|
||||
await client.request('/moderation/tech-review/issue-detail', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
@@ -381,6 +387,49 @@ function viewFileFlags(file: FlattenedFileReport) {
|
||||
currentTab.value = 'File'
|
||||
}
|
||||
|
||||
function getDetailElementId(detailId: string) {
|
||||
return `tech-review-detail-${detailId}`
|
||||
}
|
||||
|
||||
function findFileForDetail(detailId: string): FlattenedFileReport | null {
|
||||
for (const report of props.item.reports) {
|
||||
for (const issue of report.issues) {
|
||||
if (issue.details.some((detail) => detail.id === detailId)) {
|
||||
return report
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function focusDetail(detailId: string) {
|
||||
const file = findFileForDetail(detailId)
|
||||
if (!file) return
|
||||
|
||||
viewFileFlags(file)
|
||||
await nextTick()
|
||||
|
||||
const classItem = groupedByClass.value.find((group) =>
|
||||
group.flags.some((flag) => flag.detail.id === detailId),
|
||||
)
|
||||
|
||||
if (classItem) {
|
||||
expandClass(classItem)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (!import.meta.client) return
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
document.getElementById(getDetailElementId(detailId))?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function backToFileList() {
|
||||
selectedFileId.value = null
|
||||
if (currentTab.value === 'File') {
|
||||
@@ -691,6 +740,16 @@ const groupedByJar = computed<JarGroup[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.focusedDetailId,
|
||||
(detailId) => {
|
||||
if (detailId) {
|
||||
focusDetail(detailId)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Auto-expand/load source for small files; keep larger files lazy.
|
||||
watch(
|
||||
[selectedFileId, groupedByClass],
|
||||
@@ -1384,8 +1443,12 @@ function copyId() {
|
||||
>
|
||||
<div
|
||||
v-for="flag in classItem.flags"
|
||||
:id="getDetailElementId(flag.detail.id)"
|
||||
:key="`${flag.issueId}-${flag.detail.id}`"
|
||||
class="flex flex-col gap-2 rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
|
||||
:class="{
|
||||
'!border-brand bg-brand-highlight': props.focusedDetailId === flag.detail.id,
|
||||
}"
|
||||
>
|
||||
<div class="grid grid-cols-[1fr_auto] items-center">
|
||||
<div
|
||||
|
||||
@@ -2681,6 +2681,9 @@
|
||||
"moderation.page.external-projects": {
|
||||
"message": "External projects"
|
||||
},
|
||||
"moderation.page.global-detail-traces": {
|
||||
"message": "Global traces"
|
||||
},
|
||||
"moderation.page.projects": {
|
||||
"message": "Projects"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderIcon, GlobeIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||
|
||||
definePageMeta({
|
||||
@@ -47,6 +47,10 @@ const messages = defineMessages({
|
||||
id: 'moderation.page.external-projects',
|
||||
defaultMessage: 'External projects',
|
||||
},
|
||||
globalDetailTracesTitle: {
|
||||
id: 'moderation.page.global-detail-traces',
|
||||
defaultMessage: 'Global traces',
|
||||
},
|
||||
})
|
||||
|
||||
const moderationLinks = [
|
||||
@@ -62,6 +66,11 @@ const moderationLinks = [
|
||||
href: '/moderation/external-projects',
|
||||
icon: GlobeIcon,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.globalDetailTracesTitle),
|
||||
href: '/moderation/global-traces',
|
||||
icon: HashIcon,
|
||||
},
|
||||
]
|
||||
|
||||
const mobileNavOptions = [
|
||||
@@ -69,15 +78,20 @@ const mobileNavOptions = [
|
||||
formatMessage(messages.technicalReviewTitle),
|
||||
formatMessage(messages.reportsTitle),
|
||||
formatMessage(messages.externalFilesTitle),
|
||||
formatMessage(messages.globalDetailTracesTitle),
|
||||
]
|
||||
|
||||
const selectedChip = computed({
|
||||
get() {
|
||||
const path = route.path
|
||||
if (path === '/moderation/technical-review') {
|
||||
if (path.startsWith('/moderation/technical-review')) {
|
||||
return formatMessage(messages.technicalReviewTitle)
|
||||
} else if (path.startsWith('/moderation/reports/')) {
|
||||
} else if (path.startsWith('/moderation/reports')) {
|
||||
return formatMessage(messages.reportsTitle)
|
||||
} else if (path.startsWith('/moderation/external-projects')) {
|
||||
return formatMessage(messages.externalFilesTitle)
|
||||
} else if (path.startsWith('/moderation/global-traces')) {
|
||||
return formatMessage(messages.globalDetailTracesTitle)
|
||||
} else {
|
||||
return formatMessage(messages.projectsTitle)
|
||||
}
|
||||
@@ -92,6 +106,10 @@ function navigateToPage(selectedOption: string) {
|
||||
router.push('/moderation/technical-review')
|
||||
} else if (selectedOption === formatMessage(messages.reportsTitle)) {
|
||||
router.push('/moderation/reports')
|
||||
} else if (selectedOption === formatMessage(messages.externalFilesTitle)) {
|
||||
router.push('/moderation/external-projects')
|
||||
} else if (selectedOption === formatMessage(messages.globalDetailTracesTitle)) {
|
||||
router.push('/moderation/global-traces')
|
||||
} else {
|
||||
router.push('/moderation')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/moderation/global-traces">
|
||||
<ArrowLeftIcon aria-hidden="true" />
|
||||
Back to global traces
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-if="isLoading && !trace"
|
||||
type="no-search-result"
|
||||
heading="Loading global detail trace..."
|
||||
/>
|
||||
<EmptyState
|
||||
v-else-if="loadError"
|
||||
type="no-search-result"
|
||||
heading="Failed to load global detail trace"
|
||||
/>
|
||||
<article v-else-if="trace" class="universal-card flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
||||
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
||||
Trace
|
||||
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="m-0 mt-1 text-sm text-secondary">
|
||||
{{ pageStart }}-{{ pageEnd }} of {{ trace.local_trace_count }} local traces
|
||||
</p>
|
||||
</div>
|
||||
<Badge :type="trace.verdict" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="trace.local_trace_count > localTracePageSize"
|
||||
class="flex flex-wrap items-center justify-between gap-3"
|
||||
>
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
<p v-if="isLoading" class="m-0 text-sm text-secondary">Loading page...</p>
|
||||
</div>
|
||||
|
||||
<div v-if="trace.local_traces.length > 0" class="flex flex-col gap-2">
|
||||
<GlobalDetailLocalTraceCard
|
||||
v-for="localTrace in trace.local_traces"
|
||||
:key="localTrace.detail_id"
|
||||
:trace="localTrace"
|
||||
/>
|
||||
</div>
|
||||
<EmptyState v-else type="no-search-result" heading="No local traces match this key" />
|
||||
|
||||
<div v-if="trace.local_trace_count > localTracePageSize" class="mt-1 flex justify-end">
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
</div>
|
||||
</article>
|
||||
<EmptyState v-else type="no-search-result" heading="Global detail trace not found" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ArrowLeftIcon, HashIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled, EmptyState, injectModrinthClient, Pagination } from '@modrinth/ui'
|
||||
|
||||
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const route = useRoute()
|
||||
|
||||
const detailKey = computed(() => {
|
||||
const key = route.params.key
|
||||
return Array.isArray(key) ? key.join('/') : String(key)
|
||||
})
|
||||
|
||||
useHead({ title: () => `Global trace - ${detailKey.value} - Modrinth` })
|
||||
|
||||
const localTracePageSize = 20
|
||||
const isLoading = ref(false)
|
||||
const loadError = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const pageStartCursors = ref<(string | null)[]>([null])
|
||||
const trace = ref<Labrinth.TechReview.Internal.GlobalIssueDetail | null>(null)
|
||||
|
||||
const pageCount = computed(() =>
|
||||
Math.max(Math.ceil((trace.value?.local_trace_count ?? 0) / localTracePageSize), 1),
|
||||
)
|
||||
const pageStart = computed(() =>
|
||||
trace.value && trace.value.local_trace_count > 0
|
||||
? (currentPage.value - 1) * localTracePageSize + 1
|
||||
: 0,
|
||||
)
|
||||
const pageEnd = computed(() =>
|
||||
Math.min(currentPage.value * localTracePageSize, trace.value?.local_trace_count ?? 0),
|
||||
)
|
||||
|
||||
async function fetchTracePage(afterDetailId: string | null) {
|
||||
return await client.labrinth.tech_review_internal.getGlobalIssueDetail({
|
||||
detail_key: detailKey.value,
|
||||
limit: localTracePageSize,
|
||||
after_detail_id: afterDetailId,
|
||||
})
|
||||
}
|
||||
|
||||
async function loadPage(page: number) {
|
||||
if (page < 1 || isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
|
||||
try {
|
||||
while (pageStartCursors.value.length < page) {
|
||||
const cursor = pageStartCursors.value[pageStartCursors.value.length - 1]
|
||||
const response = await fetchTracePage(cursor)
|
||||
|
||||
if (!response.next_after_detail_id) {
|
||||
trace.value = response.trace
|
||||
currentPage.value = pageStartCursors.value.length
|
||||
return
|
||||
}
|
||||
|
||||
pageStartCursors.value.push(response.next_after_detail_id)
|
||||
}
|
||||
|
||||
const response = await fetchTracePage(pageStartCursors.value[page - 1])
|
||||
trace.value = response.trace
|
||||
currentPage.value = page
|
||||
} catch (error) {
|
||||
console.error('Failed to load global detail trace', error)
|
||||
loadError.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function switchPage(page: number) {
|
||||
await loadPage(page)
|
||||
}
|
||||
|
||||
watch(
|
||||
detailKey,
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
pageStartCursors.value = [null]
|
||||
trace.value = null
|
||||
loadPage(1)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<GlobalDetailTracesList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GlobalDetailTracesList from '~/components/ui/moderation/GlobalDetailTracesList.vue'
|
||||
|
||||
useHead({ title: 'Global detail traces - Modrinth' })
|
||||
</script>
|
||||
@@ -11,6 +11,7 @@ import ModerationTechRevCard from '~/components/ui/moderation/ModerationTechRevC
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
|
||||
const projectId = String(useRouteId('project'))
|
||||
|
||||
@@ -245,6 +246,8 @@ const reviewItem = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const focusedDetailId = computed(() => route.query.detail?.toString() ?? null)
|
||||
|
||||
async function handleMarkComplete(projectId: string) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['tech-reviews'] }),
|
||||
@@ -299,6 +302,7 @@ function refetch() {
|
||||
<ModerationTechRevCard
|
||||
v-else
|
||||
:item="reviewItem"
|
||||
:focused-detail-id="focusedDetailId"
|
||||
:loading-issues="loadingIssues"
|
||||
:decompiled-sources="decompiledSources"
|
||||
@refetch="refetch"
|
||||
|
||||
Reference in New Issue
Block a user