feat: moderation modpack scan button

This commit is contained in:
Gravy Boat
2026-07-06 04:37:42 -02:30
parent ad4912d64d
commit 19f0db437f
4 changed files with 163 additions and 7 deletions
@@ -0,0 +1,120 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { FolderSearchIcon, StarIcon } from '@modrinth/assets'
import { ButtonStyled, injectModrinthClient, NewModal } from '@modrinth/ui'
import { useTemplateRef } from 'vue'
const props = defineProps<{
project_id: string
}>()
const client = injectModrinthClient()
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
const files = ref<Labrinth.Versions.v2.VersionFile[]>([])
const scans = ref<Map<string, Labrinth.Attribution.Internal.FileScanResponse>>(new Map())
const isFetching = ref(false)
async function fetchAllVersions() {
const versions = await client.labrinth.versions_v2.getProjectVersions(props.project_id)
files.value = versions.flatMap((version) => version.files)
}
async function fetchAllScans() {
isFetching.value = true
for (const file of files.value) {
scans.value.set(file.id!, await client.labrinth.attribution_internal.scanFile(file.id!))
}
isFetching.value = false
}
function show() {
isFetching.value = false
fetchAllVersions()
modalRef.value?.show()
}
function hide() {
modalRef.value?.hide()
}
defineExpose({ show, hide })
</script>
<template>
<NewModal
ref="modalRef"
width="60vw"
:close-on-click-outside="false"
:close-on-esc="false"
:disable-close="isFetching"
>
<template #title>
<div class="flex w-full items-center justify-between gap-2">
<span class="text-2xl font-semibold text-contrast">
Modpack Scan ({{ scans.size }}/{{ files.length }} Files)
</span>
<div>
<ButtonStyled circular>
<button v-tooltip="'Scan All Files'" :disabled="isFetching" @click="fetchAllScans">
<FolderSearchIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</div>
</template>
<div class="w-full">
<table class="w-full table-fixed border-collapse border-0">
<thead>
<tr class="bg-surface-5 font-bold">
<td class="p-2" style="width: 80%">Pack File Name</td>
<td class="p-2 text-center" style="width: 10%">New Files</td>
<td class="p-2 text-center" style="width: 10%">New Groups</td>
</tr>
</thead>
<tbody>
<template v-for="(file, index) in files" :key="file.id">
<tr :class="index % 2 === 0 ? 'bg-surface-1' : 'bg-surface-2'">
<td class="flex h-full flex-row items-center gap-1 p-2">
<StarIcon v-if="file.primary" />{{ file.filename }}
</td>
<td class="p-2 text-center">
<span v-if="!scans.get(file.id!!)">...</span>
<span v-else>{{ scans.get(file.id!!)?.new_attribution_files!!! }}</span>
</td>
<td class="p-2 text-center">
<span v-if="!scans.get(file.id!!)">...</span>
<span v-else>{{ scans.get(file.id!!)?.new_attribution_groups!!! }}</span>
</td>
</tr>
<tr
v-if="scans.get(file.id!!)?.scanned_file_names"
:class="index % 2 === 0 ? 'bg-surface-1' : 'bg-surface-2'"
class="w-full"
>
<td colspan="3" class="p-2 pt-0">
<details>
<summary>
Override Files ({{ scans.get(file.id!!)?.scanned_file_names?.length || 0 }})
</summary>
<div class="flex flex-wrap gap-1 pt-2">
<span
v-for="name of scans.get(file.id!!)?.scanned_file_names || []"
:key="name"
v-tooltip="name"
class="flex items-center gap-1 text-wrap rounded-full bg-button-bg px-2 py-0.5 text-xs font-medium text-contrast"
>
{{ name }}
</span>
</div>
</details>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</NewModal>
</template>
+18 -7
View File
@@ -95,6 +95,8 @@
@download="triggerDownloadAnimation"
/>
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
<ModpackScanModal ref="scanModal" :project_id="project.id" />
<div
class="new-page sidebar"
:class="{
@@ -424,13 +426,6 @@
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
divider: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
id: 'tech-review',
link: `/moderation/technical-review/${project.id}`,
@@ -438,6 +433,16 @@
hoverOnly: true,
shown: auth.user && tags.staffRoles.includes(auth.user.role),
},
{
id: 'moderation-modpack-rescan',
action: () => scanModal.show(),
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
project.actualProjectType === 'modpack',
},
{
divider: true,
shown: auth.user && tags.staffRoles.includes(auth.user.role),
@@ -469,6 +474,9 @@
<ScaleIcon aria-hidden="true" /> {{ formatMessage(messages.reviewProject) }}
</template>
<template #tech-review> <ScanEyeIcon aria-hidden="true" /> Tech review </template>
<template #moderation-modpack-rescan>
<FolderSearchIcon aria-hidden="true" /> Rescan modpack
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
@@ -726,6 +734,7 @@ import {
ClipboardCopyIcon,
DownloadIcon,
ExternalIcon,
FolderSearchIcon,
HeartIcon,
ListIcon,
MoreVerticalIcon,
@@ -785,6 +794,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
import MessageBanner from '~/components/ui/MessageBanner.vue'
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
import ModpackScanModal from '~/components/ui/moderation/ModpackScanModal.vue'
import ProjectDownloadModal from '~/components/ui/ProjectDownloadModal/index.vue'
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import { getSignInRouteObj } from '~/composables/auth.ts'
@@ -852,6 +862,7 @@ const debug = useDebugLogger('DownloadModal')
const downloadModal = ref()
const openInAppModal = ref()
const overTheTopDownloadAnimation = ref()
const scanModal = ref()
const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.value != null)
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
@@ -100,4 +100,21 @@ export class LabrinthAttributionInternalModule extends AbstractModule {
body,
})
}
/**
* Scan a file for attribution information.
* POST /_internal/attribution/file/{file_id}/scan
*
* @param fileId - The file ID to scan.
*/
public async scanFile(fileId: string): Promise<Labrinth.Attribution.Internal.FileScanResponse> {
return this.client.request<Labrinth.Attribution.Internal.FileScanResponse>(
`/attribution/file/${fileId}/scan`,
{
api: 'labrinth',
version: 'internal',
method: 'POST',
},
)
}
}
@@ -397,6 +397,12 @@ export namespace Labrinth {
sha1: string
project_id: string
}
export type FileScanResponse = {
new_attribution_groups: number
new_attribution_files: number
scanned_file_names: string[]
}
}
}
@@ -1333,6 +1339,7 @@ export namespace Labrinth {
}
export type VersionFile = {
id?: string
hashes: VersionFileHash
url: string
filename: string
@@ -1454,6 +1461,7 @@ export namespace Labrinth {
}
export interface VersionFile {
id?: string
hashes: VersionFileHash
url: string
filename: string