@@ -4,13 +4,11 @@ import {
44 CheckIcon ,
55 ChevronDownIcon ,
66 ClipboardCopyIcon ,
7+ CodeIcon ,
78 CopyIcon ,
8- DownloadIcon ,
99 EllipsisVerticalIcon ,
1010 LinkIcon ,
1111 LoaderCircleIcon ,
12- ShieldCheckIcon ,
13- TriangleAlertIcon ,
1412} from ' @modrinth/assets'
1513import {
1614 Avatar ,
@@ -21,7 +19,12 @@ import {
2119 OverflowMenu ,
2220 type OverflowMenuOption ,
2321} from ' @modrinth/ui'
24- import { capitalizeString , formatProjectType , highlightCodeLines , type Thread } from ' @modrinth/utils'
22+ import {
23+ capitalizeString ,
24+ formatProjectType ,
25+ highlightCodeLines ,
26+ type Thread ,
27+ } from ' @modrinth/utils'
2528import { computed , ref } from ' vue'
2629
2730import ThreadView from ' ~/components/ui/thread/ThreadView.vue'
@@ -43,34 +46,52 @@ const emit = defineEmits<{
4346 loadSource: [issueId : string ]
4447}>()
4548
46- const quickActions: OverflowMenuOption [] = [
47- {
48- id: ' copy-link' ,
49- action : () => {
50- const base = window .location .origin
51- const reportUrl = ` ${base }/moderation/technical-review/${props .item .project .id } `
52- navigator .clipboard .writeText (reportUrl ).then (() => {
53- addNotification ({
54- type: ' success' ,
55- title: ' Technical Report link copied' ,
56- text: ' The link to this report has been copied to your clipboard.' ,
49+ const quickActions = computed <OverflowMenuOption []>(() => {
50+ const actions: OverflowMenuOption [] = []
51+
52+ // Add view source if URL exists
53+ const sourceUrl = props .item .project .link_urls ?.[' source' ]?.url
54+ if (sourceUrl ) {
55+ actions .push ({
56+ id: ' view-source' ,
57+ action : () => {
58+ window .open (sourceUrl , ' _blank' , ' noopener,noreferrer' )
59+ },
60+ })
61+ }
62+
63+ // Always add these actions
64+ actions .push (
65+ {
66+ id: ' copy-link' ,
67+ action : () => {
68+ const base = window .location .origin
69+ const reportUrl = ` ${base }/moderation/technical-review/${props .item .project .id } `
70+ navigator .clipboard .writeText (reportUrl ).then (() => {
71+ addNotification ({
72+ type: ' success' ,
73+ title: ' Technical Report link copied' ,
74+ text: ' The link to this report has been copied to your clipboard.' ,
75+ })
5776 })
58- })
77+ },
5978 },
60- },
61- {
62- id: ' copy-id ' ,
63- action : () => {
64- navigator . clipboard . writeText ( props . item . project . id ). then (() => {
65- addNotification ({
66- type : ' success ' ,
67- title : ' Technical Report ID copied' ,
68- text: ' The ID of this report has been copied to your clipboard. ' ,
79+ {
80+ id: ' copy-id ' ,
81+ action : () => {
82+ navigator . clipboard . writeText ( props . item . project . id ). then ( () => {
83+ addNotification ( {
84+ type: ' success ' ,
85+ title : ' Technical Report ID copied ' ,
86+ text : ' The ID of this report has been copied to your clipboard. ' ,
87+ })
6988 })
70- })
89+ },
7190 },
72- },
73- ]
91+ )
92+
93+ return actions
94+ })
7495
7596type Tab = ' Thread' | ' Files'
7697const tabs: readonly Tab [] = [' Thread' , ' Files' ]
@@ -91,19 +112,32 @@ const highestSeverity = computed(() => {
91112 .flatMap ((i ) => i .details )
92113 .map ((d ) => d .severity )
93114
94- const order = { SEVERE : 3 , HIGH : 2 , MEDIUM : 1 , LOW : 0 } as Record <string , number >
95- return severities .sort ((a , b ) => (order [b ] ?? 0 ) - (order [a ] ?? 0 ))[0 ] || ' LOW '
115+ const order = { severe : 3 , high : 2 , medium : 1 , low : 0 } as Record <string , number >
116+ return severities .sort ((a , b ) => (order [b ] ?? 0 ) - (order [a ] ?? 0 ))[0 ] || ' low '
96117})
97118
119+ function getSeverityBadgeColor(severity : Labrinth .TechReview .Internal .DelphiSeverity ): string {
120+ switch (severity ) {
121+ case ' severe' :
122+ return ' border-red/60 border bg-highlight-red text-red'
123+ case ' high' :
124+ case ' medium' :
125+ return ' border-orange/60 border bg-highlight-orange text-orange'
126+ case ' low' :
127+ default :
128+ return ' border-green/60 border bg-highlight-green text-green'
129+ }
130+ }
131+
98132const severityColor = computed (() => {
99133 switch (highestSeverity .value ) {
100- case ' SEVERE ' :
134+ case ' severe ' :
101135 return ' text-red bg-highlight-red border-solid border-[1px] border-red'
102- case ' HIGH ' :
136+ case ' high ' :
103137 return ' text-orange bg-highlight-orange border-solid border-[1px] border-orange'
104- case ' MEDIUM ' :
138+ case ' medium ' :
105139 return ' text-blue bg-highlight-blue border-solid border-[1px] border-blue'
106- case ' LOW ' :
140+ case ' low ' :
107141 default :
108142 return ' text-green bg-highlight-green border-solid border-[1px] border-green'
109143 }
@@ -216,7 +250,13 @@ function handleThreadUpdate() {
216250
217251 <div class =" flex flex-col gap-1.5" >
218252 <div class =" flex items-center gap-2" >
219- <span class =" text-lg font-semibold text-contrast" >{{ item.project.name }}</span >
253+ <NuxtLink
254+ :to =" `/${item.project.project_types[0]}/${item.project.slug ?? item.project.id}`"
255+ target =" _blank"
256+ class =" text-lg font-semibold text-contrast hover:underline"
257+ >
258+ {{ item.project.name }}
259+ </NuxtLink >
220260
221261 <div
222262 class =" flex items-center gap-1 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
@@ -254,24 +294,22 @@ function handleThreadUpdate() {
254294 size =" 1.5rem"
255295 circle
256296 />
257- <span class =" text-sm font-medium text-secondary" >{{ item.project_owner.name }}</span >
297+ <NuxtLink
298+ :to =" `/${item.project_owner.kind}/${item.project_owner.id}`"
299+ target =" _blank"
300+ class =" text-sm font-medium text-secondary hover:underline"
301+ >
302+ {{ item.project_owner.name }}
303+ </NuxtLink >
258304 </div >
259305 </div >
260306 </div >
261307
262308 <div class =" flex items-center gap-3" >
263309 <span class =" text-base text-secondary" >{{ formattedDate }}</span >
264310 <div class =" flex items-center gap-2" >
265- <ButtonStyled color =" green" >
266- <button ><ShieldCheckIcon /> Safe</button >
267- </ButtonStyled >
268-
269- <ButtonStyled color =" red" >
270- <button ><TriangleAlertIcon /> Malware</button >
271- </ButtonStyled >
272-
273- <ButtonStyled circular >
274- <OverflowMenu :options =" quickActions" >
311+ <ButtonStyled circular type =" outlined" >
312+ <OverflowMenu :options =" quickActions" class =" !border-px !border-surface-4" >
275313 <template #default >
276314 <EllipsisVerticalIcon class =" size-4" />
277315 </template >
@@ -283,6 +321,10 @@ function handleThreadUpdate() {
283321 <LinkIcon />
284322 <span class =" hidden sm:inline" >Copy link</span >
285323 </template >
324+ <template #view-source >
325+ <CodeIcon />
326+ <span class =" hidden sm:inline" >View source</span >
327+ </template >
286328 </OverflowMenu >
287329 </ButtonStyled >
288330 </div >
@@ -322,10 +364,7 @@ function handleThreadUpdate() {
322364
323365 <div class =" border-t border-surface-3 bg-surface-2" >
324366 <div v-if =" currentTab === 'Thread'" class =" p-4" >
325- <ThreadView
326- :thread =" item.thread as Thread"
327- @update-thread =" handleThreadUpdate"
328- />
367+ <ThreadView :thread =" item.thread as Thread" @update-thread =" handleThreadUpdate" />
329368 </div >
330369
331370 <div v-else-if =" currentTab === 'Files' && !selectedFile" class =" flex flex-col" >
@@ -371,9 +410,17 @@ function handleThreadUpdate() {
371410 <button @click =" viewFileFlags(file)" >Flags</button >
372411 </ButtonStyled >
373412
374- <ButtonStyled outline >
375- <button ><DownloadIcon /> Download</button >
376- </ButtonStyled >
413+ <!-- TODO: Impl when backend supports it -->
414+ <!-- <ButtonStyled type="outlined">
415+ <a
416+ :href="`https://api.modrinth.com/v2/version_file/${file.file_id}/download`"
417+ :title="`Download ${file.file_name}`"
418+ class="!border-px !border-surface-4"
419+ tabindex="0"
420+ >
421+ <DownloadIcon /> Download
422+ </a>
423+ </ButtonStyled> -->
377424 </div >
378425 </div >
379426 </div >
@@ -391,7 +438,10 @@ function handleThreadUpdate() {
391438 >
392439 <div class =" my-auto flex items-center gap-2" >
393440 <ButtonStyled type =" transparent" circular >
394- <button class =" transition-transform" :class =" { 'rotate-180': expandedIssues.has(issue.id) }" >
441+ <button
442+ class =" transition-transform"
443+ :class =" { 'rotate-180': expandedIssues.has(issue.id) }"
444+ >
395445 <ChevronDownIcon class =" h-5 w-5 text-contrast" />
396446 </button >
397447 </ButtonStyled >
@@ -403,18 +453,10 @@ function handleThreadUpdate() {
403453 <div
404454 v-if =" issue.details.length > 0"
405455 class =" rounded-full px-2.5 py-1"
406- :class =" {
407- 'border-red/60 border bg-highlight-red text-red':
408- issue.details[0].severity === 'SEVERE',
409- 'border-orange/60 border bg-highlight-orange text-orange':
410- issue.details[0].severity === 'HIGH' || issue.details[0].severity === 'MEDIUM',
411- 'border-green/60 border bg-highlight-green text-green':
412- issue.details[0].severity === 'LOW',
413- }"
456+ :class =" getSeverityBadgeColor(issue.details[0].severity)"
414457 >
415458 <span class =" text-sm font-medium" >{{
416- issue.details[0].severity.charAt(0) +
417- issue.details[0].severity.slice(1).toLowerCase()
459+ capitalizeString(issue.details[0].severity)
418460 }}</span >
419461 </div >
420462
@@ -424,8 +466,9 @@ function handleThreadUpdate() {
424466 class =" rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1"
425467 >
426468 <span class =" text-sm font-medium text-secondary" >
427- <LoaderCircleIcon class =" animate-spin size-5" />
428- Loading source...</span >
469+ <LoaderCircleIcon class =" size-5 animate-spin" />
470+ Loading source...</span
471+ >
429472 </div >
430473 </Transition >
431474 </div >
0 commit comments