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:
@@ -56,15 +56,17 @@ const search = async (query: string) => {
|
||||
options.value = [...resultsByProjectId.hits, ...results.hits].map((hit) => ({
|
||||
label: hit.title,
|
||||
value: hit.project_id,
|
||||
icon: defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: hit.icon_url,
|
||||
alt: hit.title,
|
||||
class: 'h-5 w-5 rounded',
|
||||
}),
|
||||
}),
|
||||
icon: markRaw(
|
||||
defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: hit.icon_url,
|
||||
alt: hit.title,
|
||||
class: 'h-5 w-5 rounded',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}))
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -323,15 +323,17 @@ const userOption = computed(() => ({
|
||||
value: 'self',
|
||||
label: auth.value.user?.username || 'Unknown user',
|
||||
icon: auth.value.user?.avatar_url
|
||||
? defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: auth.value.user?.avatar_url,
|
||||
alt: 'User Avatar',
|
||||
class: 'h-5 w-5 rounded',
|
||||
}),
|
||||
}),
|
||||
? markRaw(
|
||||
defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: auth.value.user?.avatar_url,
|
||||
alt: 'User Avatar',
|
||||
class: 'h-5 w-5 rounded-full',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
}))
|
||||
@@ -352,15 +354,17 @@ async function fetchOrganizations() {
|
||||
value: org.id,
|
||||
label: org.name,
|
||||
icon: org.icon_url
|
||||
? defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: org.icon_url,
|
||||
alt: `${org.name} Icon`,
|
||||
class: 'h-5 w-5 rounded',
|
||||
}),
|
||||
}),
|
||||
? markRaw(
|
||||
defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: org.icon_url,
|
||||
alt: `${org.name} Icon`,
|
||||
class: 'h-5 w-5 rounded',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
}))
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
search-placeholder="Search by name or paste ID..."
|
||||
loading-message="Loading..."
|
||||
no-results-message="No results found"
|
||||
include-user-unlisted-projects
|
||||
:user-id="auth?.user?.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -85,6 +87,7 @@ const currentProjectId = computed(() => projectV3.value?.id)
|
||||
const { selectedProjectId, selectedVersionId } = injectServerCompatibilityContext()
|
||||
const { labrinth } = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const auth = (await useAuth()) as { user?: { id: string } }
|
||||
|
||||
interface VersionInfo {
|
||||
id: string
|
||||
|
||||
11
apps/frontend/src/composables/queries/version.ts
Normal file
11
apps/frontend/src/composables/queries/version.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { AbstractModrinthClient } from '@modrinth/api-client'
|
||||
|
||||
import { STALE_TIME } from './project'
|
||||
|
||||
export const versionQueryOptions = {
|
||||
v3: (versionId: string, client: AbstractModrinthClient) => ({
|
||||
queryKey: ['version', 'v3', versionId] as const,
|
||||
queryFn: () => client.labrinth.versions_v3.getVersion(versionId),
|
||||
staleTime: STALE_TIME,
|
||||
}),
|
||||
}
|
||||
@@ -2132,6 +2132,12 @@
|
||||
"project-type.datapack.singular": {
|
||||
"message": "Data Pack"
|
||||
},
|
||||
"project-type.minecraft_java_server.plural": {
|
||||
"message": "Servers"
|
||||
},
|
||||
"project-type.minecraft_java_server.singular": {
|
||||
"message": "Server"
|
||||
},
|
||||
"project-type.mod.plural": {
|
||||
"message": "Mods"
|
||||
},
|
||||
@@ -2990,6 +2996,9 @@
|
||||
"settings.display.project-list-layouts.resourcepack": {
|
||||
"message": "Resource Packs page"
|
||||
},
|
||||
"settings.display.project-list-layouts.server": {
|
||||
"message": "Servers page"
|
||||
},
|
||||
"settings.display.project-list-layouts.shader": {
|
||||
"message": "Shaders page"
|
||||
},
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="mx-auto flex max-w-[40rem] flex-col gap-4 md:w-[30rem]">
|
||||
<div class="mx-auto flex max-w-[44rem] flex-col gap-4 md:w-[30rem]">
|
||||
<div
|
||||
v-if="
|
||||
project.project_type !== 'plugin' ||
|
||||
@@ -443,7 +443,27 @@
|
||||
"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand">
|
||||
<ButtonStyled
|
||||
v-if="auth.user && currentMember"
|
||||
size="large"
|
||||
color="brand"
|
||||
class="lg:!hidden"
|
||||
circular
|
||||
>
|
||||
<nuxt-link
|
||||
v-tooltip="'Edit project'"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
class="!font-bold"
|
||||
>
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="auth.user && currentMember"
|
||||
size="large"
|
||||
color="brand"
|
||||
class="max-lg:!hidden"
|
||||
>
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
class="!font-bold"
|
||||
@@ -501,7 +521,11 @@
|
||||
v-if="!isServerProject"
|
||||
size="large"
|
||||
circular
|
||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
||||
:color="
|
||||
route.name === 'type-id-version-version' || (auth.user && currentMember)
|
||||
? `standard`
|
||||
: `brand`
|
||||
"
|
||||
>
|
||||
<button
|
||||
:aria-label="formatMessage(commonMessages.downloadButton)"
|
||||
@@ -515,7 +539,11 @@
|
||||
v-else
|
||||
size="large"
|
||||
circular
|
||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
||||
:color="
|
||||
route.name === 'type-id-version-version' || (auth.user && currentMember)
|
||||
? `standard`
|
||||
: `brand`
|
||||
"
|
||||
>
|
||||
<button aria-label="Play" class="flex sm:hidden" @click="handlePlayServerProject">
|
||||
<PlayIcon aria-hidden="true" />
|
||||
@@ -1070,6 +1098,7 @@ import NavTabs from '~/components/ui/NavTabs.vue'
|
||||
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
||||
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
import { userCollectProject, userFollowProject } from '~/composables/user.js'
|
||||
import { useModerationStore } from '~/store/moderation.ts'
|
||||
import { reportProject } from '~/utils/report-helpers.ts'
|
||||
@@ -1630,52 +1659,41 @@ const serverModpackVersionId = computed(() => {
|
||||
})
|
||||
|
||||
const { data: serverModpackVersion, isPending: serverModpackVersionPending } = useQuery({
|
||||
queryKey: computed(() => ['sidebar-modpack-version', serverModpackVersionId.value]),
|
||||
queryKey: computed(() => ['version', 'v3', serverModpackVersionId.value]),
|
||||
queryFn: () => client.labrinth.versions_v3.getVersion(serverModpackVersionId.value),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: computed(() => !!serverModpackVersionId.value),
|
||||
})
|
||||
|
||||
const serverModpackProjectId = computed(() => serverModpackVersion.value?.project_id ?? null)
|
||||
|
||||
const { data: serverModpackProject, isPending: serverModpackProjectPending } = useQuery({
|
||||
queryKey: computed(() => ['sidebar-modpack-project', serverModpackProjectId.value]),
|
||||
queryFn: () => client.labrinth.projects_v3.get(serverModpackProjectId.value),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: computed(() => !!serverModpackProjectId.value),
|
||||
})
|
||||
|
||||
const serverDataLoaded = computed(() => {
|
||||
if (!projectV3.value) return false
|
||||
if (serverModpackVersionId.value && serverModpackVersionPending.value) return false
|
||||
if (serverModpackProjectId.value && serverModpackProjectPending.value) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const serverRequiredContent = computed(() => {
|
||||
if (!serverModpackProject.value) return null
|
||||
const content = projectV3.value?.minecraft_java_server?.content
|
||||
if (!content || content.kind !== 'modpack') return null
|
||||
const primaryFile =
|
||||
serverModpackVersion.value?.files?.find((f) => f.primary) ??
|
||||
serverModpackVersion.value?.files?.[0]
|
||||
return {
|
||||
name: serverModpackProject.value.name,
|
||||
name: content.project_name ?? '',
|
||||
versionNumber: serverModpackVersion.value?.version_number ?? '',
|
||||
icon: serverModpackProject.value.icon_url,
|
||||
icon: content.project_icon,
|
||||
onclickName:
|
||||
serverModpackProject.value.id !== projectId.value
|
||||
? () => navigateTo(`/modpack/${serverModpackProject.value.slug}`)
|
||||
content.project_id && content.project_id !== projectId.value
|
||||
? () => navigateTo(`/project/${content.project_id}`)
|
||||
: undefined,
|
||||
onclickVersion:
|
||||
serverModpackProject.value.id !== projectId.value
|
||||
content.project_id && content.project_id !== projectId.value
|
||||
? () =>
|
||||
navigateTo(
|
||||
`/modpack/${serverModpackProject.value.slug}/version/${serverModpackVersion.value?.id}`,
|
||||
)
|
||||
navigateTo(`/project/${content.project_id}/version/${serverModpackVersion.value?.id}`)
|
||||
: undefined,
|
||||
onclickDownload: primaryFile?.url
|
||||
? () => navigateTo(primaryFile.url, { external: true })
|
||||
: undefined,
|
||||
showCustomModpackTooltip: serverModpackProject.value.id === projectId.value,
|
||||
showCustomModpackTooltip: content.project_id === projectId.value,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1710,6 +1728,11 @@ const serverModpackLoaders = computed(() => {
|
||||
return serverModpackVersion.value.mrpack_loaders ?? []
|
||||
})
|
||||
|
||||
watch(serverModpackVersionId, (versionId) => {
|
||||
if (!versionId) return
|
||||
queryClient.prefetchQuery(versionQueryOptions.v3(versionId, client))
|
||||
})
|
||||
|
||||
// Members
|
||||
const { data: allMembersRaw, error: _membersError } = useQuery({
|
||||
queryKey: computed(() => ['project', projectId.value, 'members']),
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="input-stack">
|
||||
<FileInput
|
||||
id="project-icon"
|
||||
:max-size="262144"
|
||||
:max-size="262144000"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image iconified-button"
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
<span class="label__title">Website</span>
|
||||
<span class="label__description">Your server's official website.</span>
|
||||
</label>
|
||||
<TriangleAlertIcon
|
||||
v-if="isServerSiteLinkShortener"
|
||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<TriangleAlertIcon
|
||||
v-else-if="isServerSiteDiscordUrl"
|
||||
v-tooltip="`Discord invites are not appropriate for this link type.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<input
|
||||
id="server-website"
|
||||
v-model="siteUrl"
|
||||
@@ -22,6 +32,16 @@
|
||||
<span class="label__title">Store</span>
|
||||
<span class="label__description">A link to your server's store or shop.</span>
|
||||
</label>
|
||||
<TriangleAlertIcon
|
||||
v-if="isServerStoreLinkShortener"
|
||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<TriangleAlertIcon
|
||||
v-else-if="isServerStoreDiscordUrl"
|
||||
v-tooltip="`Discord invites are not appropriate for this link type.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<input
|
||||
id="server-store"
|
||||
v-model="storeUrl"
|
||||
@@ -41,6 +61,16 @@
|
||||
>A page containing information, documentation, and help for the server.</span
|
||||
>
|
||||
</label>
|
||||
<TriangleAlertIcon
|
||||
v-if="isServerWikiLinkShortener"
|
||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<TriangleAlertIcon
|
||||
v-else-if="isServerWikiDiscordUrl"
|
||||
v-tooltip="`Discord invites are not appropriate for this link type.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<input
|
||||
id="server-wiki"
|
||||
v-model="serverWikiUrl"
|
||||
@@ -55,6 +85,16 @@
|
||||
<span class="label__title">Discord</span>
|
||||
<span class="label__description">An invitation link to your Discord server.</span>
|
||||
</label>
|
||||
<TriangleAlertIcon
|
||||
v-if="isServerDiscordLinkShortener"
|
||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<TriangleAlertIcon
|
||||
v-else-if="!isServerDiscordUrlCommon"
|
||||
v-tooltip="`You're using a link which isn't common for this link type.`"
|
||||
class="size-6 animate-pulse text-orange"
|
||||
/>
|
||||
<input
|
||||
id="server-discord"
|
||||
v-model="serverDiscordUrl"
|
||||
@@ -337,6 +377,32 @@ const isDiscordLinkShortener = computed(() => {
|
||||
return isLinkShortener(discordUrl.value)
|
||||
})
|
||||
|
||||
const isServerSiteDiscordUrl = computed(() => {
|
||||
return isDiscordUrl(siteUrl.value)
|
||||
})
|
||||
const isServerStoreDiscordUrl = computed(() => {
|
||||
return isDiscordUrl(storeUrl.value)
|
||||
})
|
||||
const isServerWikiDiscordUrl = computed(() => {
|
||||
return isDiscordUrl(serverWikiUrl.value)
|
||||
})
|
||||
const isServerSiteLinkShortener = computed(() => {
|
||||
return isLinkShortener(siteUrl.value)
|
||||
})
|
||||
const isServerStoreLinkShortener = computed(() => {
|
||||
return isLinkShortener(storeUrl.value)
|
||||
})
|
||||
const isServerWikiLinkShortener = computed(() => {
|
||||
return isLinkShortener(serverWikiUrl.value)
|
||||
})
|
||||
const isServerDiscordLinkShortener = computed(() => {
|
||||
return isLinkShortener(serverDiscordUrl.value)
|
||||
})
|
||||
const isServerDiscordUrlCommon = computed(() => {
|
||||
if (!serverDiscordUrl.value || serverDiscordUrl.value.trim().length === 0) return true
|
||||
return isCommonUrl(serverDiscordUrl.value, commonLinkDomains.discord)
|
||||
})
|
||||
|
||||
const rawDonationLinks = JSON.parse(JSON.stringify(project.value.donation_urls))
|
||||
rawDonationLinks.push({
|
||||
id: null,
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
>
|
||||
<div class="category-selector__label">
|
||||
<component
|
||||
:is="getCategoryIcon(category.name)"
|
||||
v-if="header !== 'resolutions' && getCategoryIcon(category.name)"
|
||||
:is="getTagIcon(category.name)"
|
||||
v-if="header !== 'resolutions' && getTagIcon(category.name)"
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
/>
|
||||
@@ -111,8 +111,8 @@
|
||||
>
|
||||
<div class="category-selector__label">
|
||||
<component
|
||||
:is="getCategoryIcon(category.name)"
|
||||
v-if="category.header !== 'resolutions' && getCategoryIcon(category.name)"
|
||||
:is="getTagIcon(category.name)"
|
||||
v-if="category.header !== 'resolutions' && getTagIcon(category.name)"
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
/>
|
||||
@@ -135,7 +135,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCategoryIcon, StarIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||
import {
|
||||
getCategoryIcon,
|
||||
SERVER_CATEGORY_ICON_MAP,
|
||||
StarIcon,
|
||||
TriangleAlertIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Checkbox,
|
||||
formatCategory,
|
||||
@@ -167,9 +172,20 @@ const formatCategoryName = (categoryName: string) => {
|
||||
|
||||
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
|
||||
|
||||
const matchesProjectType = (x: Category) =>
|
||||
x.project_type === project.value.actualProjectType ||
|
||||
(x.project_type === 'minecraft_java_server' && isServerProject.value)
|
||||
const getTagIcon = (categoryName: string) => {
|
||||
const iconName = isServerProject.value
|
||||
? (SERVER_CATEGORY_ICON_MAP[categoryName] ?? categoryName)
|
||||
: categoryName
|
||||
return getCategoryIcon(iconName)
|
||||
}
|
||||
|
||||
const matchesProjectType = (x: Category) => {
|
||||
if (isServerProject.value) {
|
||||
return x.project_type === 'minecraft_java_server'
|
||||
} else {
|
||||
return x.project_type === project.value.actualProjectType
|
||||
}
|
||||
}
|
||||
|
||||
const { saved, current, saving, reset, save } = useSavable(
|
||||
() => ({
|
||||
@@ -232,6 +248,14 @@ const categoryLists = computed(() => {
|
||||
lists[header].push(x)
|
||||
}
|
||||
})
|
||||
const featuresKey = 'minecraft_server_features'
|
||||
if (lists[featuresKey]) {
|
||||
lists[featuresKey].sort((a, b) => {
|
||||
if (a.name === 'pokemon') return -1
|
||||
if (b.name === 'pokemon') return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
return lists
|
||||
})
|
||||
|
||||
@@ -333,6 +357,7 @@ const toggleFeaturedCategory = (category: Category) => {
|
||||
.category-selector__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
.icon {
|
||||
height: 1rem;
|
||||
|
||||
@@ -207,12 +207,8 @@
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div>
|
||||
<Checkbox
|
||||
:model-value="selectedProjects === projects"
|
||||
@update:model-value="
|
||||
selectedProjects === projects
|
||||
? (selectedProjects = [])
|
||||
: (selectedProjects = projects)
|
||||
"
|
||||
:model-value="allBulkEditableProjectsSelected"
|
||||
@update:model-value="toggleAllBulkEditableProjects()"
|
||||
/>
|
||||
</div>
|
||||
<div>Icon</div>
|
||||
@@ -225,13 +221,10 @@
|
||||
<div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row">
|
||||
<div>
|
||||
<Checkbox
|
||||
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
v-tooltip="getBulkEditDisabledTooltip(project)"
|
||||
:disabled="isProjectBulkEditDisabled(project)"
|
||||
:model-value="selectedProjects.includes(project)"
|
||||
@update:model-value="
|
||||
selectedProjects.includes(project)
|
||||
? (selectedProjects = selectedProjects.filter((it) => it !== project))
|
||||
: selectedProjects.push(project)
|
||||
"
|
||||
@update:model-value="toggleProjectSelection(project)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -375,6 +368,50 @@ const editLinks = reactive({
|
||||
const editLinksModal = ref(null)
|
||||
const modal_creation = ref(null)
|
||||
|
||||
function isProjectBulkEditDisabled(project) {
|
||||
return (
|
||||
(project.permissions & EDIT_DETAILS) === EDIT_DETAILS ||
|
||||
project.project_type === 'minecraft_java_server'
|
||||
)
|
||||
}
|
||||
|
||||
const bulkEditableProjects = computed(() =>
|
||||
projects.value.filter((project) => !isProjectBulkEditDisabled(project)),
|
||||
)
|
||||
|
||||
const allBulkEditableProjectsSelected = computed(
|
||||
() =>
|
||||
bulkEditableProjects.value.length > 0 &&
|
||||
bulkEditableProjects.value.every((project) => selectedProjects.value.includes(project)),
|
||||
)
|
||||
|
||||
function toggleAllBulkEditableProjects() {
|
||||
selectedProjects.value = allBulkEditableProjectsSelected.value
|
||||
? []
|
||||
: bulkEditableProjects.value.slice()
|
||||
}
|
||||
|
||||
function toggleProjectSelection(project) {
|
||||
if (isProjectBulkEditDisabled(project)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedProjects.value.includes(project)) {
|
||||
selectedProjects.value = selectedProjects.value.filter((it) => it !== project)
|
||||
return
|
||||
}
|
||||
|
||||
selectedProjects.value = [...selectedProjects.value, project]
|
||||
}
|
||||
|
||||
function getBulkEditDisabledTooltip(project) {
|
||||
if (project.project_type === 'minecraft_java_server') {
|
||||
return 'Server projects do not support bulk editing'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function updateSort(list, sort, desc) {
|
||||
let sortedArray = list
|
||||
switch (sort) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import { computed, type Reactive, watch } from 'vue'
|
||||
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
|
||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||
import { projectQueryOptions } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import { useModrinthServers } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { DisplayLocation, DisplayMode } from '~/plugins/cosmetics.ts'
|
||||
@@ -77,6 +78,25 @@ const handleProjectMouseEnter = (result: Labrinth.Search.v2.ResultSearchProject)
|
||||
prefetchTimeout.start()
|
||||
}
|
||||
|
||||
const handleServerProjectMouseEnter = (result: Labrinth.Search.v3.ResultSearchProject) => {
|
||||
const slug = result.slug || result.project_id
|
||||
|
||||
prefetchTimeout = useTimeoutFn(
|
||||
async () => {
|
||||
queryClient.prefetchQuery(projectQueryOptions.v2(slug, modrinthClient))
|
||||
queryClient.prefetchQuery(projectQueryOptions.v3(slug, modrinthClient))
|
||||
|
||||
const content = result.minecraft_java_server?.content
|
||||
if (content?.kind === 'modpack' && content.version_id) {
|
||||
queryClient.prefetchQuery(versionQueryOptions.v3(content.version_id, modrinthClient))
|
||||
}
|
||||
},
|
||||
HOVER_DURATION_TO_PREFETCH_MS,
|
||||
{ immediate: false },
|
||||
)
|
||||
prefetchTimeout.start()
|
||||
}
|
||||
|
||||
const handleProjectHoverEnd = () => {
|
||||
if (prefetchTimeout) prefetchTimeout.stop()
|
||||
}
|
||||
@@ -810,6 +830,8 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
:max-tags="2"
|
||||
is-server-project
|
||||
exclude-loaders
|
||||
@mouseenter="handleServerProjectMouseEnter(project)"
|
||||
@mouseleave="handleProjectHoverEnd"
|
||||
>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
<div class="card flex-card">
|
||||
<h2>Members</h2>
|
||||
<div class="details-list">
|
||||
<template v-for="member in acceptedMembers" :key="member.user.id">
|
||||
<template v-for="member in acceptedMembers" :key="member?.user.id">
|
||||
<nuxt-link
|
||||
class="details-list__item details-list__item--type-large"
|
||||
:to="`/user/${member?.user?.username}`"
|
||||
@@ -187,7 +187,7 @@
|
||||
<NavTabs :links="navLinks" />
|
||||
</div>
|
||||
<ProjectCardList v-if="projects && projects.length > 0">
|
||||
<ProjectCard
|
||||
<template
|
||||
v-for="project in (route.params.projectType !== undefined
|
||||
? (projects ?? []).filter((x) =>
|
||||
x.project_types.includes(
|
||||
@@ -204,28 +204,55 @@
|
||||
.slice()
|
||||
.sort((a, b) => b.downloads - a.downloads)"
|
||||
:key="project.id"
|
||||
:link="`/${project.project_types[0] ?? 'project'}/${project.slug || project.id}`"
|
||||
:title="project.name"
|
||||
:icon-url="project.icon_url"
|
||||
:banner="project.gallery.find((element) => element.featured)?.url"
|
||||
:summary="project.summary"
|
||||
:date-updated="project.updated"
|
||||
:downloads="project.downloads"
|
||||
:followers="project.followers"
|
||||
:tags="project.categories"
|
||||
:environment="{
|
||||
clientSide: project.client_side,
|
||||
serverSide: project.server_side,
|
||||
}"
|
||||
:status="
|
||||
auth.user &&
|
||||
(auth.user.id! === (user as any).id || tags.staffRoles.includes(auth.user.role))
|
||||
? (project.status as ProjectStatus)
|
||||
: undefined
|
||||
"
|
||||
:color="project.color"
|
||||
layout="list"
|
||||
/>
|
||||
>
|
||||
<ProjectCard
|
||||
v-if="isProjectServer(project)"
|
||||
:link="`/server/${project.slug || project.id}`"
|
||||
:title="project.name"
|
||||
:icon-url="project.icon_url"
|
||||
:summary="project.summary"
|
||||
:tags="project.categories"
|
||||
:server-online-players="
|
||||
project.minecraft_java_server?.ping?.data?.players_online ?? 0
|
||||
"
|
||||
:server-recent-plays="project.minecraft_java_server?.verified_plays_2w ?? 0"
|
||||
:server-region="project.minecraft_server?.region"
|
||||
:server-status-online="!!project.minecraft_java_server?.ping?.data"
|
||||
:server-modpack-content="getServerModpackContent(project)"
|
||||
:status="
|
||||
auth.user && (auth.user.id! === user.id || tags.staffRoles.includes(auth.user.role))
|
||||
? (project.status as ProjectStatus)
|
||||
: undefined
|
||||
"
|
||||
:max-tags="2"
|
||||
layout="list"
|
||||
is-server-project
|
||||
exclude-loaders
|
||||
/>
|
||||
<ProjectCard
|
||||
v-else
|
||||
:link="`/${project.project_types[0] ?? 'project'}/${project.slug || project.id}`"
|
||||
:title="project.name"
|
||||
:icon-url="project.icon_url"
|
||||
:banner="project.gallery.find((element) => element.featured)?.url"
|
||||
:summary="project.summary"
|
||||
:date-updated="project.updated"
|
||||
:downloads="project.downloads"
|
||||
:followers="project.followers"
|
||||
:tags="project.categories"
|
||||
:environment="{
|
||||
clientSide: project.client_side,
|
||||
serverSide: project.server_side,
|
||||
}"
|
||||
:status="
|
||||
auth.user && (auth.user.id! === user.id || tags.staffRoles.includes(auth.user.role))
|
||||
? (project.status as ProjectStatus)
|
||||
: undefined
|
||||
"
|
||||
:color="project.color"
|
||||
layout="list"
|
||||
/>
|
||||
</template>
|
||||
</ProjectCardList>
|
||||
<div v-else-if="true" class="error">
|
||||
<UpToDate class="icon" />
|
||||
@@ -234,7 +261,7 @@
|
||||
This organization doesn't have any projects yet.
|
||||
<template v-if="isPermission(currentMember?.permissions, 1 << 4)">
|
||||
Would you like to
|
||||
<a class="link" @click="($refs as any).modal_creation?.show()">create one</a>?
|
||||
<a class="link" @click="modal_creation?.show()">create one</a>?
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
@@ -244,6 +271,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
BoxIcon,
|
||||
ChartIcon,
|
||||
@@ -267,7 +295,7 @@ import {
|
||||
ProjectCardList,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Organization, ProjectStatus, ProjectType, ProjectV3 } from '@modrinth/utils'
|
||||
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
|
||||
@@ -282,6 +310,11 @@ import {
|
||||
} from '~/providers/organization-context.ts'
|
||||
import { isPermission } from '~/utils/permissions.ts'
|
||||
|
||||
type ProjectV3 = Labrinth.Projects.v3.Project & {
|
||||
client_side: 'required' | 'optional' | 'unsupported'
|
||||
server_side: 'required' | 'optional' | 'unsupported'
|
||||
}
|
||||
|
||||
const vintl = useVIntl()
|
||||
const { formatMessage } = vintl
|
||||
|
||||
@@ -294,6 +327,7 @@ const route = useNativeRoute()
|
||||
const router = useRouter()
|
||||
const tags = useGeneratedState()
|
||||
const config = useRuntimeConfig()
|
||||
const modal_creation = useTemplateRef('modal_creation')
|
||||
|
||||
const orgId = useRouteId()
|
||||
|
||||
@@ -397,6 +431,30 @@ const projectTypes = computed(() => {
|
||||
|
||||
return Object.keys(obj)
|
||||
})
|
||||
function isProjectServer(project: ProjectV3): boolean {
|
||||
return project.minecraft_server != null
|
||||
}
|
||||
|
||||
function getServerModpackContent(project: ProjectV3) {
|
||||
const content = project.minecraft_java_server?.content
|
||||
if (content?.kind === 'modpack') {
|
||||
const { project_name, project_icon, project_id } = content
|
||||
if (!project_name) return undefined
|
||||
return {
|
||||
name: project_name,
|
||||
icon: project_icon,
|
||||
onclick:
|
||||
project_id !== project.id
|
||||
? () => {
|
||||
navigateTo(`/project/${project_id}`)
|
||||
}
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === project.id,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const sumDownloads = computed(() => {
|
||||
let sum = 0
|
||||
|
||||
|
||||
@@ -257,6 +257,10 @@ const projectListLayouts = defineMessages({
|
||||
id: 'settings.display.project-list-layouts.modpack',
|
||||
defaultMessage: 'Modpacks page',
|
||||
},
|
||||
server: {
|
||||
id: 'settings.display.project-list-layouts.server',
|
||||
defaultMessage: 'Servers page',
|
||||
},
|
||||
user: {
|
||||
id: 'settings.display.project-list-layouts.user',
|
||||
defaultMessage: 'User profile pages',
|
||||
|
||||
@@ -49,6 +49,14 @@ const projectTypeMessages = defineMessages({
|
||||
id: 'project-type.server.plural',
|
||||
defaultMessage: 'Servers',
|
||||
},
|
||||
minecraft_java_server: {
|
||||
id: 'project-type.minecraft_java_server.singular',
|
||||
defaultMessage: 'Server',
|
||||
},
|
||||
minecraft_java_servers: {
|
||||
id: 'project-type.minecraft_java_server.plural',
|
||||
defaultMessage: 'Servers',
|
||||
},
|
||||
shader: {
|
||||
id: 'project-type.shader.singular',
|
||||
defaultMessage: 'Shader',
|
||||
|
||||
Reference in New Issue
Block a user