Skip to content

Commit 07bd516

Browse files
committed
feat: fixes to code blocks + flag labels
1 parent b3782e5 commit 07bd516

File tree

3 files changed

+173
-44
lines changed

3 files changed

+173
-44
lines changed

apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue

Lines changed: 126 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
<script setup lang="ts">
22
import type { Labrinth } from '@modrinth/api-client'
33
import {
4+
CheckIcon,
45
ChevronDownIcon,
6+
CopyIcon,
57
DownloadIcon,
68
MoreVerticalIcon,
79
ShieldCheckIcon,
810
TriangleAlertIcon,
911
} from '@modrinth/assets'
1012
import { Avatar, ButtonStyled, getProjectTypeIcon, injectModrinthClient } from '@modrinth/ui'
11-
import { formatProjectType } from '@modrinth/utils'
13+
import { capitalizeString, formatProjectType, highlightCodeLines } from '@modrinth/utils'
1214
import { computed, ref } from 'vue'
1315
1416
const props = defineProps<{
@@ -81,6 +83,18 @@ function backToFileList() {
8183
selectedFile.value = null
8284
}
8385
86+
async function copyToClipboard(code: string) {
87+
try {
88+
await navigator.clipboard.writeText(code)
89+
showCopyFeedback.value = true
90+
setTimeout(() => {
91+
showCopyFeedback.value = false
92+
}, 2000)
93+
} catch (error) {
94+
console.error('Failed to copy code:', error)
95+
}
96+
}
97+
8498
async function updateIssueStatus(
8599
issueId: string,
86100
status: Labrinth.TechReview.Internal.DelphiReportIssueStatus,
@@ -94,6 +108,7 @@ async function updateIssueStatus(
94108
}
95109
96110
const expandedIssues = ref<Set<string>>(new Set())
111+
const showCopyFeedback = ref(false)
97112
98113
function toggleIssue(issueId: string) {
99114
if (expandedIssues.value.has(issueId)) {
@@ -102,6 +117,31 @@ function toggleIssue(issueId: string) {
102117
expandedIssues.value.add(issueId)
103118
}
104119
}
120+
121+
function getSeverityBreakdown(file: Labrinth.TechReview.Internal.FileReview) {
122+
const counts = {
123+
SEVERE: 0,
124+
HIGH: 0,
125+
MEDIUM: 0,
126+
LOW: 0,
127+
}
128+
129+
file.issues.forEach((issue) => {
130+
issue.details.forEach((detail) => {
131+
if (detail.severity in counts) {
132+
counts[detail.severity as keyof typeof counts]++
133+
}
134+
})
135+
})
136+
137+
const breakdown = []
138+
if (counts.SEVERE > 0) breakdown.push({ count: counts.SEVERE, severity: 'SEVERE' })
139+
if (counts.HIGH > 0) breakdown.push({ count: counts.HIGH, severity: 'HIGH' })
140+
if (counts.MEDIUM > 0) breakdown.push({ count: counts.MEDIUM, severity: 'MEDIUM' })
141+
if (counts.LOW > 0) breakdown.push({ count: counts.LOW, severity: 'LOW' })
142+
143+
return breakdown
144+
}
105145
</script>
106146

107147
<template>
@@ -120,7 +160,7 @@ function toggleIssue(issueId: string) {
120160
<span class="text-lg font-semibold text-contrast">{{ item.project.name }}</span>
121161

122162
<div
123-
class="flex items-center gap-1 rounded-full border border-surface-5 bg-surface-4 px-2.5 py-1"
163+
class="flex items-center gap-1 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
124164
>
125165
<component
126166
:is="getProjectTypeIcon(item.project.project_types[0] as any)"
@@ -134,13 +174,15 @@ function toggleIssue(issueId: string) {
134174
>
135175
</div>
136176

137-
<div class="rounded-full border border-surface-5 bg-surface-4 px-2.5 py-1">
177+
<div
178+
class="rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
179+
>
138180
<span class="text-sm font-medium text-secondary">Auto-Flagged</span>
139181
</div>
140182

141183
<div class="rounded-full px-2.5 py-1" :class="severityColor">
142184
<span class="text-sm font-medium">{{
143-
highestSeverity.charAt(0) + highestSeverity.slice(1).toLowerCase()
185+
capitalizeString(highestSeverity.toLowerCase())
144186
}}</span>
145187
</div>
146188
</div>
@@ -183,8 +225,9 @@ function toggleIssue(issueId: string) {
183225
:key="tab"
184226
class="rounded-full px-3 py-1.5 text-base font-semibold transition-colors hover:cursor-pointer"
185227
:class="{
186-
'bg-highlight-green text-green': currentTab === tab,
187-
'text-contrast': currentTab !== tab,
228+
'bg-highlight-green text-green':
229+
currentTab === tab && !(tab === 'Files' && selectedFile),
230+
'text-contrast': currentTab !== tab || (tab === 'Files' && selectedFile),
188231
}"
189232
@click="
190233
() => {
@@ -196,12 +239,12 @@ function toggleIssue(issueId: string) {
196239
{{ tab }}
197240
</div>
198241

199-
<span
242+
<div
200243
v-if="currentTab === 'Files' && selectedFile"
201-
class="ml-2 text-sm font-medium text-secondary"
244+
class="rounded-full bg-highlight-green px-3 py-1.5 text-base font-semibold text-green"
202245
>
203246
{{ selectedFile.file_name }}
204-
</span>
247+
</div>
205248
</div>
206249
</div>
207250

@@ -227,13 +270,27 @@ function toggleIssue(issueId: string) {
227270
>
228271
<div class="flex items-center gap-3">
229272
<span class="font-medium text-contrast">{{ file.file_name }}</span>
230-
<div class="rounded-full border border-surface-5 bg-surface-3 px-2.5 py-1">
273+
<div class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1">
231274
<span class="text-sm font-medium text-secondary">{{
232275
formatFileSize(file.file_size)
233276
}}</span>
234277
</div>
235-
<div class="border-red/60 rounded-full border bg-highlight-red px-2.5 py-1">
236-
<span class="text-sm font-medium text-red">{{ file.issues.length }} flags</span>
278+
<div
279+
v-for="severityItem in getSeverityBreakdown(file)"
280+
:key="severityItem.severity"
281+
class="rounded-full border border-solid px-2.5 py-1"
282+
:class="{
283+
'border-red/60 bg-highlight-red text-red': severityItem.severity === 'SEVERE',
284+
'border-orange/60 bg-highlight-orange text-orange':
285+
severityItem.severity === 'HIGH',
286+
'border-blue/60 bg-highlight-blue text-blue': severityItem.severity === 'MEDIUM',
287+
'border-green/60 bg-highlight-green text-green': severityItem.severity === 'LOW',
288+
}"
289+
>
290+
<span class="text-sm font-medium"
291+
>{{ severityItem.count }}
292+
{{ capitalizeString(severityItem.severity.toLowerCase()) }}</span
293+
>
237294
</div>
238295
</div>
239296

@@ -253,18 +310,20 @@ function toggleIssue(issueId: string) {
253310
<div
254311
v-for="(issue, idx) in selectedFile.issues"
255312
:key="issue.issue_id"
256-
class="border-x border-b border-surface-3 bg-surface-2"
313+
class="border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2"
257314
:class="{ 'rounded-bl-2xl rounded-br-2xl': idx === selectedFile.issues.length - 1 }"
258315
>
259316
<div class="flex items-center justify-between p-4">
260-
<div class="flex items-center gap-2">
261-
<button
262-
class="transition-transform"
263-
:class="{ 'rotate-180': !expandedIssues.has(issue.issue_id) }"
264-
@click="toggleIssue(issue.issue_id)"
265-
>
266-
<ChevronDownIcon class="h-5 w-5 text-contrast" />
267-
</button>
317+
<div class="my-auto flex items-center gap-2">
318+
<ButtonStyled type="transparent" circular>
319+
<button
320+
class="transition-transform"
321+
:class="{ 'rotate-180': !expandedIssues.has(issue.issue_id) }"
322+
@click="toggleIssue(issue.issue_id)"
323+
>
324+
<ChevronDownIcon class="h-5 w-5 text-contrast" />
325+
</button>
326+
</ButtonStyled>
268327

269328
<span class="text-base font-semibold text-contrast">{{
270329
issue.kind.replace(/_/g, ' ')
@@ -290,18 +349,16 @@ function toggleIssue(issueId: string) {
290349
</div>
291350

292351
<div class="flex items-center gap-2">
293-
<ButtonStyled
294-
:outline="issue.status !== 'safe'"
295-
:color="issue.status === 'safe' ? 'green' : undefined"
296-
>
297-
<button @click="updateIssueStatus(issue.issue_id, 'safe')">Safe</button>
352+
<ButtonStyled color="brand" type="outlined">
353+
<button class="!border-[1px]" @click="updateIssueStatus(issue.issue_id, 'safe')">
354+
Safe
355+
</button>
298356
</ButtonStyled>
299357

300-
<ButtonStyled
301-
:outline="issue.status !== 'unsafe'"
302-
:color="issue.status === 'unsafe' ? 'red' : undefined"
303-
>
304-
<button @click="updateIssueStatus(issue.issue_id, 'unsafe')">Malware</button>
358+
<ButtonStyled color="red" type="outlined">
359+
<button class="!border-[1px]" @click="updateIssueStatus(issue.issue_id, 'unsafe')">
360+
Malware
361+
</button>
305362
</ButtonStyled>
306363
</div>
307364
</div>
@@ -310,23 +367,42 @@ function toggleIssue(issueId: string) {
310367
<div
311368
v-for="(detail, detailIdx) in issue.details"
312369
:key="detailIdx"
313-
class="flex flex-col gap-3"
370+
class="flex flex-col"
314371
>
315-
<div class="flex items-center gap-4">
316-
<p class="font-mono text-sm text-secondary">{{ detail.class_name }}</p>
317-
</div>
372+
<p class="mt-0 pt-0 font-mono text-sm text-secondary">{{ detail.class_name }}</p>
318373

319374
<div
320375
v-if="detail.decompiled_source"
321-
class="flex gap-3 overflow-x-auto rounded-2xl bg-surface-3 p-3 font-mono text-sm"
376+
class="relative overflow-hidden rounded-lg border border-solid border-surface-5 bg-surface-4"
322377
>
323-
<div class="flex flex-col border-r border-surface-5 pr-3 text-right text-secondary">
324-
<span v-for="n in detail.decompiled_source.split('\n').length" :key="n">{{
325-
n
326-
}}</span>
378+
<ButtonStyled circular type="transparent">
379+
<button
380+
class="absolute right-2 top-2 border-[1px]"
381+
@click="copyToClipboard(detail.decompiled_source)"
382+
v-tooltip="`Copy code`"
383+
>
384+
<CopyIcon v-if="!showCopyFeedback" />
385+
<CheckIcon v-else />
386+
</button>
387+
</ButtonStyled>
388+
389+
<div class="overflow-x-auto bg-surface-3 py-3">
390+
<div
391+
v-for="(line, n) in highlightCodeLines(detail.decompiled_source, 'java')"
392+
:key="n"
393+
class="flex font-mono text-[13px] leading-[1.6]"
394+
>
395+
<div
396+
class="select-none border-0 border-r border-solid border-surface-5 px-4 py-0 text-right text-primary"
397+
style="min-width: 3.5rem"
398+
>
399+
{{ n + 1 }}
400+
</div>
401+
<div class="flex-1 px-4 py-0 text-primary">
402+
<pre v-html="line || ' '"></pre>
403+
</div>
404+
</div>
327405
</div>
328-
329-
<pre class="flex-1 text-secondary"><code>{{ detail.decompiled_source }}</code></pre>
330406
</div>
331407
</div>
332408
</div>
@@ -335,3 +411,11 @@ function toggleIssue(issueId: string) {
335411
</div>
336412
</div>
337413
</template>
414+
415+
<style scoped>
416+
pre {
417+
all: unset;
418+
display: inline;
419+
white-space: pre;
420+
}
421+
</style>

apps/frontend/src/pages/moderation/technical-review.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,16 @@ const paginatedItems = computed(() => {
211211
const end = start + itemsPerPage
212212
return filteredItems.value.slice(start, end)
213213
})
214-
function goToPage(page: number) {
214+
function goToPage(page: number, top = false) {
215215
currentPage.value = page
216+
217+
if (top && window) {
218+
window.scrollTo({
219+
top: 0,
220+
left: 0,
221+
behavior: 'smooth',
222+
})
223+
}
216224
}
217225
218226
// TEMPORARY: Commented out while using mock data
@@ -352,7 +360,11 @@ const batchScanProgressInformation = computed<BatchScanProgress | undefined>(()
352360
</div>
353361

354362
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
355-
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
363+
<Pagination
364+
:page="currentPage"
365+
:count="totalPages"
366+
@switch-page="(num) => goToPage(num, true)"
367+
/>
356368
</div>
357369
</div>
358370
</template>

packages/utils/highlightjs/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ hljs.registerAliases(['toml'], { languageName: 'ini' })
5555
hljs.registerAliases(['yml'], { languageName: 'yaml' })
5656
hljs.registerAliases(['html', 'htm', 'xhtml', 'mcui', 'fxml'], { languageName: 'xml' })
5757

58+
export { hljs }
59+
5860
export const renderHighlightedString = (string) =>
5961
configuredXss.process(
6062
md({
@@ -71,3 +73,34 @@ export const renderHighlightedString = (string) =>
7173
},
7274
}).render(string),
7375
)
76+
77+
export const highlightCodeLines = (code: string, language: string): string[] => {
78+
if (!code) return []
79+
80+
if (!hljs.getLanguage(language)) {
81+
return code.split('\n')
82+
}
83+
84+
try {
85+
const highlighted = hljs.highlight(code, { language }).value
86+
const openTags: string[] = []
87+
88+
const processedHtml = highlighted.replace(/(<span [^>]+>)|(<\/span>)|(\n)/g, (match) => {
89+
if (match === '\n') {
90+
return '</span>'.repeat(openTags.length) + '\n' + openTags.join('')
91+
}
92+
93+
if (match === '</span>') {
94+
openTags.pop()
95+
} else {
96+
openTags.push(match)
97+
}
98+
99+
return match
100+
})
101+
102+
return processedHtml.split('\n')
103+
} catch {
104+
return code.split('\n')
105+
}
106+
}

0 commit comments

Comments
 (0)