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:
@@ -1,36 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-[1fr_auto] max-lg:gap-x-8 gap-y-6 border-0 border-b border-solid border-divider pb-4"
|
||||
>
|
||||
<div class="flex gap-4 w-full">
|
||||
<slot name="icon" />
|
||||
<div class="flex flex-col gap-2 justify-center w-full">
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="flex flex-col gap-1.5 justify-center">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1 class="m-0 text-2xl font-semibold leading-none text-contrast">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<slot name="title-suffix" />
|
||||
<div class="flex flex-col gap-2 border-0 border-b border-solid border-divider pb-4">
|
||||
<div class="grid grid-cols-[1fr_auto] gap-y-6">
|
||||
<div class="flex gap-4 w-full">
|
||||
<slot name="icon" />
|
||||
<div class="flex flex-col gap-2 justify-center w-full">
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="flex flex-col gap-1.5 justify-center">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1 class="m-0 text-2xl font-semibold leading-none text-contrast">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<slot name="title-suffix" />
|
||||
</div>
|
||||
<p
|
||||
v-if="$slots.summary"
|
||||
class="m-0 max-w-[44rem] empty:hidden"
|
||||
:class="[disableLineClamp ? '' : 'line-clamp-2']"
|
||||
>
|
||||
<slot name="summary" />
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="$slots.summary" class="flex gap-2 items-start max-md:hidden">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
<p v-if="$slots.summary" class="m-0 line-clamp-2 max-w-[40rem] empty:hidden">
|
||||
<slot name="summary" />
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="$slots.summary" class="flex gap-2 items-start max-lg:hidden">
|
||||
<slot name="actions" />
|
||||
<div v-if="$slots.stats" class="flex flex-wrap gap-3 empty:hidden max-md:hidden">
|
||||
<slot name="stats" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.stats" class="flex flex-wrap gap-3 empty:hidden">
|
||||
<slot name="stats" />
|
||||
</div>
|
||||
<div class="flex gap-2 items-start lg:hidden">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!$slots.summary" class="flex gap-2 items-start max-md:hidden">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!$slots.summary" class="flex gap-2 items-start max-lg:hidden">
|
||||
<slot name="actions" />
|
||||
<div class="flex justify-between">
|
||||
<div v-if="$slots.stats" class="flex flex-wrap gap-3 empty:hidden md:hidden">
|
||||
<slot name="stats" />
|
||||
</div>
|
||||
<div class="flex gap-2 items-start self-end md:hidden">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
disableLineClamp?: boolean
|
||||
}
|
||||
|
||||
const { disableLineClamp } = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="modal.hide()">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
@@ -124,6 +124,9 @@ function proceed() {
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
function hide() {
|
||||
modal.value.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{{ project.description }}
|
||||
</template>
|
||||
<template #stats>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-3 flex-wrap gap-y-0">
|
||||
<div
|
||||
v-tooltip="
|
||||
capitalizeString(
|
||||
|
||||
@@ -58,8 +58,12 @@
|
||||
<section v-if="props.ping !== undefined || region" class="flex flex-col gap-2">
|
||||
<h3 class="text-primary text-base m-0">Region</h3>
|
||||
<div class="flex flex-wrap gap-1.5 items-center">
|
||||
<ServerPing
|
||||
v-if="projectV3?.status !== 'draft'"
|
||||
:ping="props.ping"
|
||||
:status-online="props.statusOnline"
|
||||
/>
|
||||
<ServerRegion v-if="region" :region="region" />
|
||||
<ServerPing :ping="props.ping" :status-online="props.statusOnline" />
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="languages.length > 0" class="flex flex-col gap-2">
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<div v-if="allTags.length > 0" class="flex flex-col gap-3">
|
||||
<h2 class="text-lg m-0">Tags</h2>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<TagItem v-for="tag in allTags" :key="tag">
|
||||
<TagItem
|
||||
v-for="tag in allTags"
|
||||
:key="tag"
|
||||
:action="props.project.actualProjectType ? () => handleClickTag(tag) : undefined"
|
||||
>
|
||||
<FormattedTag :tag="tag" />
|
||||
</TagItem>
|
||||
</div>
|
||||
@@ -10,14 +14,31 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import FormattedTag from '../base/FormattedTag.vue'
|
||||
import TagItem from '../base/TagItem.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleClickTag = (tag: string) => {
|
||||
if (!props.project.actualProjectType) return
|
||||
|
||||
const projectType =
|
||||
props.project.actualProjectType === 'minecraft_java_server'
|
||||
? 'server'
|
||||
: props.project.actualProjectType
|
||||
|
||||
const params = projectType === 'server' ? `sc=${tag}` : `f=categories:${tag}`
|
||||
|
||||
router.push(`/discover/${projectType}?${params}`)
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
project: {
|
||||
categories: string[]
|
||||
additional_categories: string[]
|
||||
actualProjectType?: string
|
||||
}
|
||||
}>()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ContentPageHeader>
|
||||
<ContentPageHeader disable-line-clamp>
|
||||
<template #icon>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" size="96px" />
|
||||
<Avatar :src="project.icon_url" :alt="project.title" size="108px" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ project.title }}
|
||||
@@ -15,6 +15,7 @@
|
||||
<template #stats>
|
||||
<div class="flex items-center gap-3 gap-y-1 flex-wrap">
|
||||
<ServerDetails
|
||||
v-if="projectV3?.status !== 'draft'"
|
||||
:online-players="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
:recent-plays="javaServer?.verified_plays_2w ?? 0"
|
||||
@@ -24,7 +25,7 @@
|
||||
<TagItem
|
||||
v-for="(category, index) in project.categories"
|
||||
:key="index"
|
||||
:action="() => router.push(`/${project.project_type}s?f=categories:${category}`)"
|
||||
:action="() => router.push(`/discover/servers?sc=${category}`)"
|
||||
>
|
||||
<FormattedTag :tag="category" />
|
||||
</TagItem>
|
||||
|
||||
@@ -49,16 +49,21 @@
|
||||
</div>
|
||||
<div class="mt-auto flex flex-col gap-3 flex-wrap overflow-hidden justify-between grow">
|
||||
<div class="flex items-center gap-1 flex-wrap overflow-hidden">
|
||||
<ServerDetails
|
||||
v-if="isServerProject"
|
||||
:region="serverRegion"
|
||||
:online-players="serverOnlinePlayers"
|
||||
:recent-plays="serverRecentPlays"
|
||||
:ping="serverPing"
|
||||
:status-online="serverStatusOnline"
|
||||
:hide-online-players-label="true"
|
||||
:hide-recent-plays-label="true"
|
||||
/>
|
||||
<template v-if="isServerProject">
|
||||
<ServerOnlinePlayers
|
||||
v-if="serverOnlinePlayers !== undefined"
|
||||
:online="serverOnlinePlayers"
|
||||
:status-online="serverStatusOnline"
|
||||
:hide-label="true"
|
||||
/>
|
||||
<ServerRecentPlays
|
||||
v-if="serverRecentPlays !== undefined"
|
||||
:recent-plays="serverRecentPlays"
|
||||
:hide-label="true"
|
||||
/>
|
||||
<ServerPing v-if="serverPing && serverStatusOnline" :ping="serverPing" />
|
||||
<ServerRegion v-if="serverRegion" :region="serverRegion" />
|
||||
</template>
|
||||
<ProjectCardEnvironment
|
||||
v-if="environment"
|
||||
:client-side="environment.clientSide"
|
||||
@@ -134,16 +139,21 @@
|
||||
</div>
|
||||
<div class="mt-auto flex items-center gap-3 grid-project-card-list__tags">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<ServerDetails
|
||||
v-if="isServerProject"
|
||||
:region="serverRegion"
|
||||
:online-players="serverOnlinePlayers"
|
||||
:status-online="serverStatusOnline"
|
||||
:recent-plays="serverRecentPlays"
|
||||
:ping="serverPing"
|
||||
:hide-online-players-label="true"
|
||||
:hide-recent-plays-label="true"
|
||||
/>
|
||||
<template v-if="isServerProject">
|
||||
<ServerOnlinePlayers
|
||||
v-if="serverOnlinePlayers !== undefined"
|
||||
:online="serverOnlinePlayers"
|
||||
:status-online="serverStatusOnline"
|
||||
:hide-label="true"
|
||||
/>
|
||||
<ServerRecentPlays
|
||||
v-if="serverRecentPlays !== undefined"
|
||||
:recent-plays="serverRecentPlays"
|
||||
:hide-label="true"
|
||||
/>
|
||||
<ServerPing v-if="serverPing && serverStatusOnline" :ping="serverPing" />
|
||||
<ServerRegion v-if="serverRegion" :region="serverRegion" />
|
||||
</template>
|
||||
<div class="flex items-center gap-1">
|
||||
<ProjectCardEnvironment
|
||||
v-if="environment"
|
||||
@@ -181,8 +191,11 @@ import { computed } from 'vue'
|
||||
import { AutoLink, Avatar } from '../../base'
|
||||
import { SmartClickable } from '../../base/index.ts'
|
||||
import ProjectStatusBadge from '../ProjectStatusBadge.vue'
|
||||
import ServerDetails from '../server/ServerDetails.vue'
|
||||
import ServerModpackContent from '../server/ServerModpackContent.vue'
|
||||
import ServerOnlinePlayers from '../server/ServerOnlinePlayers.vue'
|
||||
import ServerPing from '../server/ServerPing.vue'
|
||||
import ServerRecentPlays from '../server/ServerRecentPlays.vue'
|
||||
import ServerRegion from '../server/ServerRegion.vue'
|
||||
import ProjectCardAuthor from './ProjectCardAuthor.vue'
|
||||
import ProjectCardDate from './ProjectCardDate.vue'
|
||||
import ProjectCardEnvironment, {
|
||||
|
||||
@@ -21,7 +21,7 @@ defineProps<{
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="empty:hidden flex items-center gap-2">
|
||||
<div class="empty:hidden flex items-center gap-2 flex-wrap gap-y-1">
|
||||
<ServerOnlinePlayers
|
||||
v-if="onlinePlayers !== undefined"
|
||||
:online="onlinePlayers"
|
||||
@@ -33,8 +33,8 @@ defineProps<{
|
||||
:recent-plays="recentPlays"
|
||||
:hide-label="hideRecentPlaysLabel"
|
||||
/>
|
||||
<ServerRegion v-if="region" :region="region" />
|
||||
<ServerPing v-if="ping && statusOnline" :ping="ping" />
|
||||
<ServerRegion v-if="region" :region="region" />
|
||||
<ServerModpackContent
|
||||
v-if="modpackContent"
|
||||
:name="modpackContent.name"
|
||||
|
||||
@@ -21,7 +21,7 @@ const pingClass = computed(() => {
|
||||
if (props.ping === undefined) {
|
||||
return 'border-brand bg-highlight-green text-brand'
|
||||
}
|
||||
if (props.ping < 100) {
|
||||
if (props.ping < 150) {
|
||||
return 'border-brand bg-highlight-green text-brand'
|
||||
}
|
||||
if (props.ping < 250) {
|
||||
|
||||
@@ -21,5 +21,5 @@ const regionNames: Record<string, string> = {
|
||||
const regionName = computed(() => regionNames[region] ?? region)
|
||||
</script>
|
||||
<template>
|
||||
<TagItem>{{ regionName }}</TagItem>
|
||||
<TagItem v-tooltip="`Server hosted in ${regionName}`">{{ regionName }}</TagItem>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user