Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
a6ccb8e
chore: fix typo in status message
AlexTMjugador Aug 18, 2025
47f3481
feat(labrinth): overhaul malware scanner report storage and routes
AlexTMjugador Aug 18, 2025
b4e9fad
chore: address some review comments
AlexTMjugador Aug 22, 2025
17173c7
feat: add Delphi to Docker Compose `with-delphi` profile
AlexTMjugador Aug 22, 2025
2c646eb
chore: fix unused import Clippy lint
AlexTMjugador Aug 22, 2025
1ccf17c
feat(labrinth/delphi): use PAT token authorization with project read …
AlexTMjugador Aug 22, 2025
71fb6b2
chore: expose file IDs in version queries
AlexTMjugador Aug 22, 2025
8269cc8
fix: accept null decompiled source payloads from Delphi
AlexTMjugador Aug 23, 2025
ce0e26c
tweak(labrinth): expose base62 file IDs more consistently for Delphi
AlexTMjugador Aug 23, 2025
7152edd
feat(labrinth/delphi): support new Delphi report severity field
AlexTMjugador Aug 23, 2025
da08a03
chore(labrinth): run `cargo sqlx prepare` to fix Docker build errors
AlexTMjugador Aug 25, 2025
93cf106
tweak: add route for fetching Delphi issue type schema, abstract Labr…
AlexTMjugador Sep 6, 2025
b4284af
chore: run `cargo sqlx prepare`
AlexTMjugador Sep 6, 2025
ff5471c
chore: fix typo on frontend generated state file message
AlexTMjugador Oct 5, 2025
ce05e51
feat: update to use new Delphi issue schema
AlexTMjugador Oct 24, 2025
45580f7
wip: tech review endpoints
aecsocket Nov 7, 2025
465aa51
wip: add ToSchema for dependent types
aecsocket Nov 10, 2025
520cc8e
wip: report issues return
aecsocket Nov 10, 2025
ae53eab
wip
aecsocket Nov 12, 2025
32d1ae2
wip: returning more data
aecsocket Nov 13, 2025
7d347b0
wip
aecsocket Nov 13, 2025
9f4699b
Fix up db query
aecsocket Nov 13, 2025
06f34d9
Delphi configuration to talk to Labrinth
aecsocket Nov 13, 2025
e5d8d9a
Get Delphi working with Labrinth
aecsocket Nov 15, 2025
f1741ed
Add Delphi dummy fixture
aecsocket Nov 15, 2025
a722a6a
Better Delphi logging
aecsocket Nov 15, 2025
2c284d0
Improve utoipa for tech review routes
aecsocket Nov 16, 2025
a2ae5ce
Add more sorting options for tech review queue
aecsocket Nov 16, 2025
419c24f
Oops join
aecsocket Nov 16, 2025
ec9afad
New routes for fetching issues and reports
aecsocket Nov 18, 2025
fdf2d11
Fix which kind of ID is returned in tech review endpoints
aecsocket Nov 19, 2025
0b1a41d
Deduplicate tech review report rows
aecsocket Nov 19, 2025
b19eaa8
Reduce info sent for projects
aecsocket Nov 19, 2025
387c55f
Fetch more thread info
aecsocket Nov 21, 2025
f8f5bfa
Address PR comments
aecsocket Nov 23, 2025
4602160
fix ci
aecsocket Nov 25, 2025
ef9ade5
fix postgres version mismatch
aecsocket Nov 26, 2025
8fe7f20
fix version creation
aecsocket Nov 26, 2025
1a58c13
Implement routes
aecsocket Nov 30, 2025
c588c84
fix up tech review
aecsocket Dec 1, 2025
c0ce9db
Allow adding a moderation comment to Delphi rejections
aecsocket Dec 4, 2025
799a2a9
fix up rebase
aecsocket Dec 4, 2025
2e672c7
exclude rejected projects from tech review
aecsocket Dec 5, 2025
68ac53f
add status change msg to tech review thread
aecsocket Dec 7, 2025
8b80e8d
cargo sqlx prepare
aecsocket Dec 7, 2025
dc918f8
also ignore withheld projects
aecsocket Dec 9, 2025
8b4e1d7
More filtering on issue search
aecsocket Dec 9, 2025
f0a388c
wip: report routes
aecsocket Dec 11, 2025
b7c6197
Merge branch 'main' into boris/tech-review-queue
aecsocket Dec 11, 2025
c962bf4
Fix up for build
aecsocket Dec 11, 2025
f00c248
cargo sqlx prepare
aecsocket Dec 11, 2025
4a9cd5b
fix thread message privacy
aecsocket Dec 11, 2025
fc31d61
New tech review search route
aecsocket Dec 12, 2025
550889c
submit route
aecsocket Dec 12, 2025
e90101e
details have statuses now
aecsocket Dec 13, 2025
8a699f5
add default to drid status
aecsocket Dec 15, 2025
1624186
dedup issue details
aecsocket Dec 15, 2025
35b977e
fix sqlx query on empty files
aecsocket Dec 15, 2025
fdcbfd2
Merge branch 'main' into boris/tech-review-queue
aecsocket Dec 17, 2025
10a222a
fixes
aecsocket Dec 17, 2025
ca6724f
Dedupe issue detail statuses and message on entering tech rev
aecsocket Dec 18, 2025
33c2b87
Fix qa issues
aecsocket Dec 19, 2025
41ecb77
Fix qa issues
aecsocket Dec 19, 2025
c2d1f13
fix review comments
aecsocket Dec 19, 2025
fc41515
Merge branch 'main' into boris/tech-review-queue
aecsocket Dec 19, 2025
7f4ee16
typos
aecsocket Dec 19, 2025
2c48c29
Merge branch 'main' into boris/tech-review-queue
aecsocket Dec 20, 2025
f9a6eb3
fix ci
aecsocket Dec 20, 2025
ebc7b3d
feat: tech review frontend (#4781)
IMB11 Dec 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse

### Postgres

Use `docker exec labrinth-postgres psql -U postgres` to access the PostgreSQL instance.
Use `docker exec labrinth-postgres psql -U labrinth -d labrinth -c "SELECT 1"` to access the PostgreSQL instance, replacing the `SELECT 1` with your query.

# Guidelines

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export default defineNuxtConfig({
(state.errors ?? []).length === 0
) {
console.log(
'Tags already recently generated. Delete apps/frontend/generated/state.json to force regeneration.',
'Tags already recently generated. Delete apps/frontend/src/generated/state.json to force regeneration.',
)
return
}
Expand Down
169 changes: 126 additions & 43 deletions apps/frontend/src/components/ui/NavTabs.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,58 @@
<template>
<nav
ref="scrollContainer"
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
:class="[mode === 'navigation' ? 'card-shadow' : undefined]"
>
<NuxtLink
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
:key="index"
ref="tabLinkElements"
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
:class="{
'text-button-textSelected': activeIndex === index && !subpageSelected,
'text-contrast': activeIndex === index && subpageSelected,
}"
>
<component :is="link.icon" v-if="link.icon" class="size-5" />
<span class="text-nowrap">{{ link.label }}</span>
</NuxtLink>
<template v-if="mode === 'navigation'">
<NuxtLink
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
:key="link.href"
ref="tabLinkElements"
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
>
<component
:is="link.icon"
v-if="link.icon"
class="size-5"
:class="{
'text-brand': currentActiveIndex === index && !subpageSelected,
'text-secondary': currentActiveIndex !== index || subpageSelected,
}"
/>
<span class="text-nowrap text-contrast">{{ link.label }}</span>
</NuxtLink>
</template>
<template v-else>
<div
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
:key="link.href"
ref="tabLinkElements"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 hover:cursor-pointer focus:rounded-full"
@click="emit('tabClick', index, link)"
>
<component
:is="link.icon"
v-if="link.icon"
class="size-5"
:class="{
'text-brand': currentActiveIndex === index && !subpageSelected,
'text-secondary': currentActiveIndex !== index || subpageSelected,
}"
/>
<span
class="text-nowrap"
:class="{
'text-brand': currentActiveIndex === index && !subpageSelected,
'text-contrast': currentActiveIndex !== index || subpageSelected,
}"
>{{ link.label }}</span
>
</div>
</template>
<div
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
Expand All @@ -27,29 +62,44 @@
top: sliderTopPx,
right: sliderRightPx,
bottom: sliderBottomPx,
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
opacity:
sliderLeft === 4 && sliderLeft === sliderRight ? 0 : currentActiveIndex === -1 ? 0 : 1,
}"
aria-hidden="true"
></div>
</nav>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import type { Component } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'

const route = useNativeRoute()

interface Tab {
label: string
href: string
shown?: boolean
icon?: string
icon?: Component
subpages?: string[]
}

const props = defineProps<{
links: Tab[]
query?: string
const props = withDefaults(
defineProps<{
links: Tab[]
query?: string
mode?: 'navigation' | 'local'
activeIndex?: number
}>(),
{
mode: 'navigation',
query: undefined,
activeIndex: undefined,
},
)

const emit = defineEmits<{
tabClick: [index: number, tab: Tab]
}>()

const scrollContainer = ref<HTMLElement | null>(null)
Expand All @@ -58,7 +108,7 @@ const sliderLeft = ref(4)
const sliderTop = ref(4)
const sliderRight = ref(4)
const sliderBottom = ref(4)
const activeIndex = ref(-1)
const currentActiveIndex = ref(-1)
const subpageSelected = ref(false)

const filteredLinks = computed(() =>
Expand All @@ -74,38 +124,49 @@ const tabLinkElements = ref()
function pickLink() {
let index = -1
subpageSelected.value = false
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
if (props.query) {
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {

if (props.mode === 'local' && props.activeIndex !== undefined) {
index = Math.min(props.activeIndex, filteredLinks.value.length - 1)
} else {
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
if (props.query) {
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
index = i
break
}
} else if (decodeURIComponent(route.path) === link.href) {
index = i
break
} else if (
decodeURIComponent(route.path).includes(link.href) ||
(link.subpages &&
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
) {
index = i
subpageSelected.value = true
break
}
} else if (decodeURIComponent(route.path) === link.href) {
index = i
break
} else if (
decodeURIComponent(route.path).includes(link.href) ||
(link.subpages &&
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
) {
index = i
subpageSelected.value = true
break
}
}
activeIndex.value = index

if (activeIndex.value !== -1) {
startAnimation()
currentActiveIndex.value = index

if (currentActiveIndex.value !== -1) {
nextTick(() => startAnimation())
} else {
sliderLeft.value = 0
sliderRight.value = 0
}
}

function startAnimation() {
const el = tabLinkElements.value[activeIndex.value]?.$el
// In navigation mode, elements are NuxtLinks with $el property
// In local mode, elements are plain divs
const el =
props.mode === 'navigation'
? tabLinkElements.value[currentActiveIndex.value]?.$el
: tabLinkElements.value[currentActiveIndex.value]

if (!el || !el.offsetParent) return

Expand Down Expand Up @@ -156,7 +217,29 @@ onMounted(() => {

watch(
() => [route.path, route.query],
() => pickLink(),
() => {
if (props.mode === 'navigation') {
pickLink()
}
},
)

watch(
() => props.activeIndex,
() => {
if (props.mode === 'local') {
pickLink()
}
},
)

watch(
() => props.links,
() => {
// Re-trigger animation when links change
pickLink()
},
{ deep: true },
)
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ import {
financialMessages,
formFieldLabels,
formFieldPlaceholders,
getBlockchainColor,
getBlockchainIcon,
normalizeChildren,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
Expand All @@ -218,12 +220,6 @@ import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { useGeneratedState } from '@/composables/generated'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
import {
getBlockchainColor,
getBlockchainIcon,
getCurrencyColor,
getCurrencyIcon,
} from '@/utils/finance-icons.ts'
import { getRailConfig } from '@/utils/muralpay-rails'

const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees } = useWithdrawContext()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<div
class="flex flex-col gap-4 rounded-2xl border-[1px] border-solid border-blue bg-highlight-blue p-4"
>
<div class="flex flex-row justify-between">
<div class="flex flex-col text-contrast">
<span class="text-xl font-semibold">Batch scan in progress</span>
<span>{{ progress?.complete }} of {{ progress?.total }} projects completed</span>
</div>
<ButtonStyled circular color="blue" type="outlined">
<button class="!px-4" @click="emit('cancel-scan')">Cancel scan</button>
</ButtonStyled>
</div>
<div class="w-full rounded-full bg-highlight-blue">
<div
class="h-3 rounded-[inherit] bg-blue"
:style="`width: ${((progress?.complete ?? 0) / (progress?.total ?? 1)) * 100}%`"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { ButtonStyled } from '@modrinth/ui'
import { defineProps } from 'vue'

export interface BatchScanProgress {
total: number
complete: number
}

defineProps<{
progress?: BatchScanProgress
}>()

const emit = defineEmits<{
(e: 'cancel-scan'): void
}>()
</script>
Loading