fix: project middleware startLoading

This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:22 +01:00
parent c0d839c7f6
commit 27f629a7a3
3 changed files with 165 additions and 13 deletions
+24 -5
View File
@@ -1,6 +1,7 @@
import { useGeneratedState } from '~/composables/generated'
import { projectQueryOptions } from '~/composables/queries/project'
import { useAppQueryClient } from '~/composables/query-client'
import { createModrinthClient } from '~/helpers/api.ts'
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
import { useServerModrinthClient } from '~/server/utils/api-client'
@@ -18,9 +19,6 @@ const PROJECT_TYPES = [
]
export default defineNuxtRouteMiddleware(async (to) => {
// Only run this middleware on the server - it relies on server-only runtime config
if (import.meta.client) return
const routeProjectParam = to.params.project
const projectId = Array.isArray(routeProjectParam) ? routeProjectParam[0] : routeProjectParam
const routeType = Array.isArray(to.params.type) ? to.params.type[0] : to.params.type
@@ -31,10 +29,11 @@ export default defineNuxtRouteMiddleware(async (to) => {
}
const queryClient = useAppQueryClient()
const authToken = useCookie('auth-token')
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
const client = await getProjectMiddlewareClient()
const tags = useGeneratedState()
if (import.meta.client) startLoading()
try {
// Fetch v2 and v3 in parallel — cache both for the page's useQuery calls
const [project, projectV3] = await Promise.all([
@@ -48,9 +47,11 @@ export default defineNuxtRouteMiddleware(async (to) => {
// Cache by slug if we looked up by ID (or vice versa)
if (projectId !== project.slug) {
queryClient.setQueryData(['project', 'v2', project.slug], project)
queryClient.setQueryData(['project', 'v3', project.slug], projectV3)
}
if (projectId !== project.id) {
queryClient.setQueryData(['project', 'v2', project.id], project)
queryClient.setQueryData(['project', 'v3', project.id], projectV3)
}
const projectType = projectV3.minecraft_server != null ? 'server' : project.project_type
@@ -81,5 +82,23 @@ export default defineNuxtRouteMiddleware(async (to) => {
}
} catch {
// Let the page handle 404s and other errors
} finally {
if (import.meta.client) stopLoading()
}
})
async function getProjectMiddlewareClient() {
if (import.meta.server) {
const authToken = useCookie('auth-token')
return useServerModrinthClient({ authToken: authToken.value || undefined })
}
const auth = await useAuth()
const config = useRuntimeConfig()
return createModrinthClient(auth, {
apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'),
archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'),
rateLimitKey: config.rateLimitKey,
})
}
+114 -6
View File
@@ -421,6 +421,18 @@
</template>
</NewModal>
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
<div
v-if="projectInstallContext && !isSettings"
ref="stickyInstallHeaderRef"
class="sticky top-0 z-20 mx-auto max-w-[80rem] border-0 border-solid border-divider bg-surface-1 px-6 pt-4"
:class="[isInstallHeaderStuck ? 'border-t' : '']"
>
<BrowseInstallHeader :install-context="projectHeaderInstallContext" divider bottom-padding />
</div>
<SelectedProjectsFloatingBar
v-if="projectInstallContext && !isSettings"
:install-context="projectInstallContext"
/>
<div
class="new-page sidebar"
:class="{
@@ -435,7 +447,10 @@
!flags.alwaysShowChecklistAsPopup,
}"
>
<div class="normal-page__header relative my-4">
<div
class="normal-page__header relative mb-4"
:class="projectInstallContext && !isSettings ? 'mt-0' : 'mt-4'"
>
<div class="mb-6">
<ModerationProjectNags
v-if="
@@ -721,6 +736,7 @@ import {
import {
Admonition,
Avatar,
BrowseInstallHeader,
ButtonStyled,
Checkbox,
commonMessages,
@@ -743,12 +759,14 @@ import {
ProjectSidebarTags,
provideProjectPageContext,
ScrollablePanel,
SelectedProjectsFloatingBar,
ServersPromo,
StyledInput,
useDebugLogger,
useFormatDateTime,
useFormatPrice,
useRelativeTime,
useStickyObserver,
useVIntl,
} from '@modrinth/ui'
import VersionSummary from '@modrinth/ui/src/components/version/VersionSummary.vue'
@@ -772,6 +790,7 @@ import { getSignInRouteObj } from '~/composables/auth.ts'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
import { versionQueryOptions } from '~/composables/queries/version'
import { useServerInstallContent } from '~/composables/use-server-install-content'
import { userCollectProject, userFollowProject } from '~/composables/user.js'
import {
loadChecklistOpenState,
@@ -843,6 +862,11 @@ const versionFilter = ref('')
const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.value != null)
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
const stickyInstallHeaderRef = ref(null)
const { isStuck: isInstallHeaderStuck } = useStickyObserver(
stickyInstallHeaderRef,
'ProjectInstallHeader',
)
const projectEnvironmentModal = useTemplateRef('projectEnvironmentModal')
const modalCollection = useTemplateRef('modal_collection')
@@ -987,6 +1011,10 @@ const messages = defineMessages({
id: 'project.navigation.changelog',
defaultMessage: 'Changelog',
},
backToDiscover: {
id: 'project.install-context.back-to-discover',
defaultMessage: 'Back to discover',
},
createNewCollection: {
id: 'project.collections.create-new',
defaultMessage: 'Create new collection',
@@ -1340,6 +1368,60 @@ const project = computed(() => {
),
}
})
const routeProjectType = computed(() =>
Array.isArray(route.params.type) ? route.params.type[0] : route.params.type,
)
const projectInstallType = computed(() => ({
id: project.value?.actualProjectType ?? routeProjectType.value,
}))
const serverInstallModalRef = ref(null)
const serverInstallDebug = useDebugLogger('ProjectServerInstall')
const { installContext: serverBrowseInstallContext } = useServerInstallContent({
projectType: projectInstallType,
onboardingModalRef: serverInstallModalRef,
debug: serverInstallDebug,
})
const projectDiscoverBackUrl = computed(() => {
const browsePath = route.query.b
if (typeof browsePath === 'string' && browsePath.startsWith('/discover/')) {
return browsePath
}
const discoverType =
routeProjectType.value === 'project'
? (project.value?.actualProjectType ?? project.value?.project_type ?? 'mod')
: (routeProjectType.value ?? project.value?.actualProjectType ?? 'mod')
return `/discover/${discoverType}s${getInstallContextQueryString([
'sid',
'wid',
'from',
'shi',
])}`
})
const projectInstallContext = computed(() => {
const context = serverBrowseInstallContext.value
if (!context) return null
return {
...context,
backUrl: projectDiscoverBackUrl.value,
backLabel: formatMessage(messages.backToDiscover),
discardSelectedAndBack: async () => {
await (context.clearSelected ?? context.clearQueued)?.()
await navigateTo(projectDiscoverBackUrl.value)
},
}
})
const projectHeaderInstallContext = computed(() => {
const context = projectInstallContext.value
if (!context) return null
return {
...context,
onBack: undefined,
selectedProjects: [],
isInstallingSelected: false,
}
})
// Use actual project ID for dependent queries (ensures cache consistency)
const projectId = computed(() => projectRaw.value?.id)
@@ -2493,6 +2575,32 @@ function onVersionNavigate(url) {
})
}
const INSTALL_CONTEXT_QUERY_KEYS = ['sid', 'wid', 'from', 'shi', 'b']
function getInstallContextQueryString(keys = INSTALL_CONTEXT_QUERY_KEYS) {
const params = new URLSearchParams()
for (const key of keys) {
const value = route.query[key]
if (Array.isArray(value)) {
for (const item of value) {
if (item != null) {
params.append(key, item)
}
}
} else if (value != null) {
params.append(key, value)
}
}
const queryString = params.toString()
return queryString ? `?${queryString}` : ''
}
function withInstallContextQuery(path) {
return `${path}${getInstallContextQueryString()}`
}
async function deleteVersion(id) {
if (!id) return
@@ -2517,16 +2625,16 @@ const navLinks = computed(() => {
return [
{
label: formatMessage(messages.descriptionTab),
href: projectUrl,
href: withInstallContextQuery(projectUrl),
},
{
label: formatMessage(messages.galleryTab),
href: `${projectUrl}/gallery`,
href: withInstallContextQuery(`${projectUrl}/gallery`),
shown: galleryCount > 0 || !!currentMember.value,
},
{
label: formatMessage(messages.changelogTab),
href: `${projectUrl}/changelog`,
href: withInstallContextQuery(`${projectUrl}/changelog`),
shown:
hasVersions.value &&
projectV3Loaded.value &&
@@ -2535,7 +2643,7 @@ const navLinks = computed(() => {
},
{
label: formatMessage(messages.versionsTab),
href: `${projectUrl}/versions`,
href: withInstallContextQuery(`${projectUrl}/versions`),
shown:
(hasVersions.value || !!currentMember.value) &&
projectV3Loaded.value &&
@@ -2545,7 +2653,7 @@ const navLinks = computed(() => {
},
{
label: formatMessage(messages.moderationTab),
href: `${projectUrl}/moderation`,
href: withInstallContextQuery(`${projectUrl}/moderation`),
shown: !!currentMember.value,
},
]
@@ -33,6 +33,7 @@ import { cycleValue } from '@modrinth/utils'
import { useQueryClient } from '@tanstack/vue-query'
import { useTimeoutFn } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import type { LocationQueryRaw } from 'vue-router'
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
@@ -219,6 +220,28 @@ function mapV3ProjectHit(hit: Labrinth.Search.v3.ResultSearchProject): DiscoverP
}
}
const hostingContextQuery = computed(() => {
const query: LocationQueryRaw = {}
const hasHostingContext = route.query.sid != null
for (const key of ['sid', 'wid', 'from', 'shi']) {
const value = route.query[key]
if (value != null) {
query[key] = value
}
}
if (hasHostingContext) {
query.b = route.fullPath
}
return Object.keys(query).length > 0 ? query : undefined
})
function withHostingContext(path: string) {
return hostingContextQuery.value ? { path, query: hostingContextQuery.value } : path
}
async function fetchSearch(requestParams: string) {
debug('search() called', {
requestParams: requestParams.substring(0, 100),
@@ -468,9 +491,11 @@ provideBrowseManager({
projectType: projectTypeId,
...searchState,
getProjectLink: (result: Labrinth.Search.v2.ResultSearchProject) =>
`/${projectType.value?.id ?? 'project'}/${result.slug ? result.slug : result.project_id}`,
withHostingContext(
`/${projectType.value?.id ?? 'project'}/${result.slug ? result.slug : result.project_id}`,
),
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) =>
`/server/${result.slug ?? result.project_id}`,
withHostingContext(`/server/${result.slug ?? result.project_id}`),
selectableProjectTypes: computed(() => []),
showProjectTypeTabs: computed(() => false),
variant: 'web',