fix navtabs issues and unify project lists (#7245)

* fix navtabs issues and unify project lists

* move page navtab logic to component
This commit is contained in:
Prospector
2026-08-20 21:32:32 +00:00
committed by GitHub
parent bcd7133251
commit 087752312b
19 changed files with 645 additions and 519 deletions
+2 -2
View File
@@ -25,8 +25,8 @@ export async function get_user_profile(userId: string): Promise<Labrinth.Users.v
return await invoke<Labrinth.Users.v3.User>('plugin:users|get_user_profile', { userId })
}
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v2.Project[]> {
return await invoke<Labrinth.Projects.v2.Project[]>('plugin:users|get_user_projects', {
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v3.Project[]> {
return await invoke<Labrinth.Projects.v3.Project[]>('plugin:users|get_user_projects', {
userId,
})
}
+17 -11
View File
@@ -11,19 +11,20 @@
>
<template #project-actions="{ project }">
<Button
v-if="project.minecraft_server == null"
type="outlined"
class="!text-brand [&>svg]:!text-brand !shadow-[inset_0_0_0_1px_var(--color-brand)]"
:disabled="isProjectInstalling(project.id)"
@click.stop="installProject(project)"
>
<SpinnerIcon v-if="isProjectInstalling(project.id)" class="animate-spin" />
<DownloadIcon v-else-if="project.project_type === 'modpack'" />
<DownloadIcon v-else-if="project.project_types.includes('modpack')" />
<PlusIcon v-else />
{{
formatMessage(
isProjectInstalling(project.id)
? commonMessages.installingLabel
: project.project_type === 'modpack'
: project.project_types.includes('modpack')
? commonMessages.installButton
: messages.installToInstance,
)
@@ -96,7 +97,7 @@ function setProjectInstalling(projectId: string, installing: boolean): void {
installingProjectIds.value = next
}
async function installProject(project: Labrinth.Projects.v2.Project): Promise<void> {
async function installProject(project: Labrinth.Projects.v3.Project): Promise<void> {
if (isProjectInstalling(project.id)) return
setProjectInstalling(project.id, true)
@@ -130,14 +131,19 @@ const userProfile = provideUserProfile({
unblockUser: unblock_user,
})
const userId = computed(() => {
const value = route.params.user
return Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
})
const projectType = computed(() => {
const value = route.params.projectType
return Array.isArray(value) ? value[0] : value
})
function readRouteParam(param: unknown): string | undefined {
const value = Array.isArray(param) ? param[0] : param
return typeof value === 'string' && value.length > 0 ? value : undefined
}
const userId = ref(readRouteParam(route.params.user) ?? '')
watch(
() => readRouteParam(route.params.user),
(next) => {
if (next) userId.value = next
},
)
const projectType = computed(() => readRouteParam(route.params.projectType))
function getCachedUserSummary(id: string) {
return queryClient.getQueryData<Labrinth.Users.v3.User>(['users', 'summary', id])
+1 -1
View File
@@ -532,7 +532,7 @@
</div>
<div class="normal-page__content">
<div class="mb-3 overflow-x-auto"><NavTabs :links="navLinks" replace class="mb-1" /></div>
<NavTabs :links="navLinks" replace page-nav />
<NuxtPage @on-download="triggerDownloadAnimation" @delete-version="deleteVersion" />
</div>
</div>
@@ -307,64 +307,32 @@
}
}),
]"
class="mb-4"
replace
page-nav
/>
<ProjectCardList
<ProjectList
v-if="projects && projects?.length > 0"
:projects="displayedProjects"
:layout="cosmetics.searchDisplayMode.collection"
>
<ProjectCard
v-for="project in (route.params.projectType !== undefined
? projects.filter(
(x) =>
x.project_type ===
route.params.projectType.substr(0, route.params.projectType.length - 1),
)
: projects
)
.slice()
.sort((a, b) => b.downloads - a.downloads)"
:key="project.id"
:link="`/${project.project_type}/${project.slug ?? project.id}`"
:title="project.title"
:icon-url="project.icon_url"
:banner="project.gallery.find((element) => element.featured)?.url"
:summary="project.description"
:date-updated="project.updated"
:downloads="project.downloads ?? 0"
:followers="project.followers ?? 0"
:tags="project.categories"
:environment="{
clientSide: project.client_side,
serverSide: project.server_side,
}"
:color="project.color"
:layout="
cosmetics.searchDisplayMode.collection === 'grid' ||
cosmetics.searchDisplayMode.collection === 'gallery'
? 'grid'
: 'list'
"
>
<template v-if="canEdit || collection.id === 'following'" #actions>
<Button
v-if="canEdit"
class="remove-btn"
:disabled="removing"
@click="() => removeProject(project)"
>
<SpinnerIcon v-if="removing" class="animate-spin" aria-hidden="true" />
<XIcon v-else aria-hidden="true" />
{{ formatMessage(messages.removeProjectButton) }}
</Button>
<Button v-if="collection.id === 'following'" @click="unfollowProject(project)">
<HeartMinusIcon aria-hidden="true" />
{{ formatMessage(messages.unfollowProjectButton) }}
</Button>
</template>
</ProjectCard>
</ProjectCardList>
<template v-if="canEdit || collection.id === 'following'" #actions="{ project }">
<Button
v-if="canEdit"
class="remove-btn"
:disabled="removing"
@click="() => removeProject(project)"
>
<SpinnerIcon v-if="removing" class="animate-spin" aria-hidden="true" />
<XIcon v-else aria-hidden="true" />
{{ formatMessage(messages.removeProjectButton) }}
</Button>
<Button v-if="collection.id === 'following'" @click="unfollowProject(project)">
<HeartMinusIcon aria-hidden="true" />
{{ formatMessage(messages.unfollowProjectButton) }}
</Button>
</template>
</ProjectList>
<EmptyState v-else type="empty-inbox" :heading="formatMessage(messages.noProjectsLabel)">
<template #actions>
<ButtonLink
@@ -405,6 +373,7 @@ import {
Avatar,
Button,
ButtonLink,
catalogProjectTypes,
commonMessages,
commonProjectTypeCategoryMessages,
commonProjectTypeSentenceMessages,
@@ -413,6 +382,7 @@ import {
defineMessages,
EmptyState,
FileInput,
filterProjectsByType,
HorizontalRule,
injectModrinthClient,
injectNotificationManager,
@@ -421,11 +391,10 @@ import {
NewModal,
normalizeChildren,
NormalPage,
ProjectCard,
ProjectCardList,
parseProjectTypeRouteParam,
ProjectList,
RadioButtons,
SidebarCard,
sortProjectTypes,
StyledInput,
TeleportOverflowMenu,
useCompactNumber,
@@ -463,21 +432,13 @@ async function fetchProjectsByIds(projectIds) {
segments.push(projectIds.slice(i, i + segmentSize))
}
const results = await Promise.all(
segments.map((ids) => api.labrinth.projects_v2.getMultiple(ids)),
segments.map((ids) => api.labrinth.projects_v3.getMultiple(ids)),
)
const projects = results.flat()
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
}
return projects
return results.flat()
}
async function fetchFollowedProjects(userId) {
const projects = await api.labrinth.users_v2.getFollowedProjects(userId)
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
}
return projects
return api.labrinth.users_v3.getFollowedProjects(userId)
}
const messages = defineMessages({
@@ -765,13 +726,13 @@ const canEdit = computed(
collection.value.id !== 'following',
)
const projectTypes = computed(() => {
const projectSet = new Set(
projects.value?.map((project) => project?.project_type).filter((x) => x !== undefined) || [],
)
projectSet.delete('project')
return sortProjectTypes(projectSet)
})
const projectTypes = computed(() => catalogProjectTypes(projects.value ?? []))
const displayedProjects = computed(() =>
filterProjectsByType(projects.value ?? [], parseProjectTypeRouteParam(route.params.projectType))
.slice()
.sort((a, b) => b.downloads - a.downloads),
)
function getProjectTypeSentenceMessage(type) {
return commonProjectTypeSentenceMessages[type] ?? commonProjectTypeSentenceMessages.project
@@ -621,7 +621,7 @@ const { isStuck: isInstallHeaderStuck } = useStickyObserver(
"
>
<section
class="flex min-w-0 flex-col gap-3"
class="mt-6 flex min-w-0 flex-col gap-3 sm:mt-0"
:class="cosmetics.rightSearchLayout ? 'lg:order-1' : 'lg:order-2'"
>
<BrowsePageLayout>
@@ -122,74 +122,12 @@
</Button>
</div>
</div>
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
<NavTabs :links="navLinks" replace />
</div>
<ProjectCardList v-if="projects && projects.length > 0">
<template
v-for="project in (route.params.projectType !== undefined
? (projects ?? []).filter((x) =>
x.project_types.includes(
typeof route.params.projectType === 'string'
? route.params.projectType.slice(0, route.params.projectType.length - 1)
: route.params.projectType[0]?.slice(
0,
route.params.projectType[0].length - 1,
) || '',
),
)
: (projects ?? [])
)
.slice()
.sort(projectUserSorting)"
:key="project.id"
>
<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="project.environment?.[0]"
: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>
<NavTabs v-if="navLinks.length > 2" :links="navLinks" replace page-nav />
<ProjectList
v-if="projects && projects.length > 0"
:projects="displayedProjects"
:show-status="showProjectStatus"
/>
<div v-else-if="true" class="error">
<UpToDate class="icon" />
<br />
@@ -226,21 +164,21 @@ import {
import {
Avatar,
Button,
catalogProjectTypes,
commonMessages,
defineMessages,
filterProjectsByType,
injectModrinthClient,
IntlFormatted,
NavTabs,
normalizeChildren,
PROJECT_DEP_MARKER_QUERY,
ProjectCard,
ProjectCardList,
parseProjectTypeRouteParam,
ProjectList,
SidebarCard,
sortProjectTypes,
useCompactNumber,
useVIntl,
} from '@modrinth/ui'
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
import type { Organization, ProjectType } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
@@ -332,19 +270,7 @@ const {
isFetching: projectsIsFetching,
} = useQuery({
queryKey: computed(() => ['organization', orgId, 'projects']),
queryFn: async () => {
// @ts-expect-error
const rawProjects = (await client.labrinth.organizations_v3.getProjects(orgId)) as ProjectV3[]
return rawProjects.map((project) => {
let categories = project.categories.concat(project.loaders)
if (project.mrpack_loaders) {
categories = categories.concat(project.mrpack_loaders as string[])
}
return { ...project, categories }
})
},
queryFn: () => client.labrinth.organizations_v3.getProjects(orgId),
placeholderData: [],
})
@@ -386,42 +312,17 @@ const isInvited = computed(() => {
return currentMember.value?.accepted === false
})
const projectTypes = computed(() => {
const types = new Set<string>()
const projectTypes = computed(() => catalogProjectTypes(projects.value ?? []))
for (const project of projects.value ?? []) {
const type = project.project_types[0] ?? 'project'
if (type !== 'project') types.add(type)
}
const displayedProjects = computed(() =>
filterProjectsByType(projects.value ?? [], parseProjectTypeRouteParam(route.params.projectType))
.slice()
.sort(projectUserSorting),
)
return sortProjectTypes(types)
})
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({
path: `/project/${project_id}`,
query: { ...PROJECT_DEP_MARKER_QUERY },
})
}
: undefined,
showCustomModpackTooltip: project_id === project.id,
}
}
return undefined
}
const showProjectStatus = computed(
() => !!(auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))),
)
const sumDownloads = computed(() => {
let sum = 0
+14 -6
View File
@@ -31,7 +31,7 @@ const client = injectModrinthClient()
const queryClient = useQueryClient()
const userProfile = provideUserProfile({
getUser: (userId) => client.labrinth.users_v3.get(userId),
getProjects: (userId) => client.labrinth.users_v2.getProjects(userId),
getProjects: (userId) => client.labrinth.users_v3.getProjects(userId),
getOrganizations: (userId) => client.labrinth.users_v2.getOrganizations(userId),
getCollections: (userId) => client.labrinth.users_v2.getCollections(userId),
patchUser: (userId, patch) => client.labrinth.users_v2.patch(userId, patch),
@@ -46,11 +46,19 @@ const auth = await useAuth()
const cosmetics = useCosmetics()
const config = useRuntimeConfig()
const userId = computed(() => String(route.params.user))
const projectType = computed(() => {
const value = route.params.projectType
return Array.isArray(value) ? value[0] : value
})
function readRouteParam(param: unknown): string | undefined {
const value = Array.isArray(param) ? param[0] : param
return typeof value === 'string' && value.length > 0 ? value : undefined
}
const userId = ref(readRouteParam(route.params.user) ?? '')
watch(
() => readRouteParam(route.params.user),
(next) => {
if (next) userId.value = next
},
)
const projectType = computed(() => readRouteParam(route.params.projectType))
let prefetchedUser: Labrinth.Users.v3.User | undefined
try {