fix: servers misc fixes (#5475)

* fix: tags in project settings to have icons and ordered correctly

* fix copy in project list layout settings

* fix tag item in header navigation

* adjust ping ranges

* add handle click tag

* fix: dont show offline in project page for draft status

* move tags above creators in app

* preload server project page on load and optimize queries

* add server project card to organization page

* fix minecraft_java_server label

* pnpm prepr

* have user option in project create modal be circle

* feat: implement better mobile project page view

* disable summary line clamp for servers

* fix: unlink instance doesnt update instance

* increase icon upload size

* small fix on button size

* improve how server ping info loads

* remove unnecessary pings for instance page

* fix order of computing dependency diff

* remove linked_project_id from world, use name+address to match for managed world instead

* pnpm prepr

* hide duplicate worlds with same domain name in worlds list

* add install content warning for server instance

* increase summary max width

* add handling for server projects for bulk editing links

* implement include user unlisted projects in published modpack select

* pnpm prepr

* filter to only user unlisted status

* add bad link warnings

* fix modpack tags appearing in server

* cargo fmt
This commit is contained in:
Truman Gao
2026-03-07 02:11:45 +00:00
committed by GitHub
parent 98175a58a6
commit 83d53dafe7
44 changed files with 993 additions and 377 deletions
@@ -16,7 +16,8 @@
<script lang="ts" setup>
import { PackageIcon } from '@modrinth/assets'
import { useDebounceFn } from '@vueuse/core'
import { defineAsyncComponent, h, ref, watch } from 'vue'
import Fuse from 'fuse.js'
import { defineAsyncComponent, h, markRaw, ref, watch } from 'vue'
import { injectModrinthClient, injectNotificationManager } from '../../providers'
import type { ComboboxOption } from '../base/Combobox.vue'
@@ -57,6 +58,10 @@ const props = withDefaults(
limit?: number
/** Project IDs to exclude from results */
excludeProjectIds?: string[]
/** Include the user's own projects (including unlisted) in results via Fuse search */
includeUserUnlistedProjects?: boolean
/** User ID or username required when includeUserUnlistedProjects is true */
userId?: string
}>(),
{
placeholder: 'Select project',
@@ -78,22 +83,67 @@ const searchResultsCache = ref<Map<string, SearchHit>>(new Map())
const { labrinth } = injectModrinthClient()
const userProjectHits = ref<SearchHit[]>([])
const userProjectsFuse = ref<Fuse<SearchHit> | null>(null)
watch(
() => props.includeUserUnlistedProjects && props.userId,
async (shouldFetch) => {
if (!shouldFetch || !props.userId) {
userProjectHits.value = []
userProjectsFuse.value = null
return
}
try {
const projects = await labrinth.users_v2.getProjects(props.userId)
const projectTypeSet = props.projectTypes ? new Set(props.projectTypes) : null
userProjectHits.value = projects
.filter((p) => !projectTypeSet || projectTypeSet.has(p.project_type as ProjectType))
.filter((p) => p.status === 'unlisted')
.map((p) => ({
project_id: p.id,
title: p.title,
icon_url: p.icon_url ?? undefined,
project_type: p.project_type,
slug: p.slug,
}))
for (const hit of userProjectHits.value) {
searchResultsCache.value.set(hit.project_id, hit)
}
userProjectsFuse.value = new Fuse(userProjectHits.value, {
keys: ['title', 'slug'],
threshold: 0.4,
})
} catch {
userProjectHits.value = []
userProjectsFuse.value = null
}
},
{ immediate: true },
)
function hitToOption(hit: SearchHit): ComboboxOption<string> {
return {
label: hit.title,
value: hit.project_id,
icon: hit.icon_url
? defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
? markRaw(
defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
),
)
: PackageIcon,
: markRaw(PackageIcon),
}
}
@@ -160,7 +210,11 @@ const search = async (query: string) => {
facets: [[`project_id:${query.replace(/[^a-zA-Z0-9]/g, '')}`]],
})
const allHits = [...resultsByProjectId.hits, ...results.hits]
const userFuseHits: SearchHit[] = userProjectsFuse.value
? userProjectsFuse.value.search(query).map((r) => r.item)
: []
const allHits = [...userFuseHits, ...resultsByProjectId.hits, ...results.hits]
const seenIds = new Set<string>()
const excludeSet = new Set(props.excludeProjectIds ?? [])
const uniqueHits: SearchHit[] = []
@@ -169,7 +223,6 @@ const search = async (query: string) => {
if (!seenIds.has(hit.project_id) && !excludeSet.has(hit.project_id)) {
seenIds.add(hit.project_id)
uniqueHits.push(hit)
// Cache the hit for later lookup
searchResultsCache.value.set(hit.project_id, hit)
}
}