refactor: removing useAsyncData for tanstack query (#5262)

* refactor: most places with useAsyncData replaced with tanstack query

* refactor report list and report view

* refactor organization page to use tanstack query

* fix types

* refactor collection page and include proper loading state

* fix followed projects proper loading state

* fix 404 handling

* fix organization loading and 404 states

* pnpm prepr

* refactor: remove useAsyncData on newsletter button

* refactor: remove useAsyncData on auth globals fetch

* refactor: settings/billing/index.vue to useQuery instead of useAsyncData

* refactor: user page to remove useAsyncData

* pnpm prepr

* fix reports pages

* fix notifications page

* fix billing page cannot read properties of null and prop warnings

* fix refresh causing 404 by removing useBaseFetch and use api-client

* fix stale data after removing organization from project

* pnpm prepr

* fix news erroring in build

* fix: project page loads header only after content

* fix: user page tanstack problems (start on migrating away from useBaseFetch)

* fix: start swapping useBaseFetch usages to api-client

* Revert "fix: start swapping useBaseFetch usages to api-client"

This reverts commit 3df3fab11d535159132b1288dd7cacc38282b553.

* fix: remove debug logging

* fix: lint

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2026-03-16 12:10:29 -07:00
committed by GitHub
parent d0c7575a23
commit 681ae5d1d8
53 changed files with 1686 additions and 1079 deletions

View File

@@ -1,5 +1,5 @@
<template>
<template v-if="project">
<template v-if="project && projectV3Loaded">
<Teleport v-if="flags.projectBackground" to="#fixed-background-teleport">
<ProjectBackgroundGradient :project="project" />
</Teleport>
@@ -1672,10 +1672,9 @@ const {
error: _projectV3Error,
isPending: projectV3Pending,
} = useQuery({
queryKey: computed(() => ['project', 'v3', projectId.value]),
queryFn: () => client.labrinth.projects_v3.get(projectId.value),
queryKey: computed(() => ['project', 'v3', routeProjectId.value]),
queryFn: () => client.labrinth.projects_v3.get(routeProjectId.value),
staleTime: STALE_TIME,
enabled: computed(() => !!projectId.value),
})
// Server sidebar: modpack version + project for required content
@@ -1811,13 +1810,17 @@ const {
// Organization
// Only fetch organization if project belongs to one
const { data: organization } = useQuery({
const { data: organizationRaw } = useQuery({
queryKey: computed(() => ['project', projectId.value, 'organization']),
queryFn: () => client.labrinth.projects_v3.getOrganization(projectId.value),
staleTime: STALE_TIME,
enabled: computed(() => !!projectId.value && !!projectRaw.value?.organization),
})
// When project is removed from org, enabled becomes false but TanStack keeps stale data.
// Return null when the project no longer belongs to an organization.
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
// Transform versionsV3 to be same shape as versionsV2 for compatibility in project pages
const versionsRaw = computed(() => {
return (versionsV3.value ?? []).map((v) => {

View File

@@ -101,8 +101,11 @@
<script setup>
import { CheckIcon, IssuesIcon, XIcon } from '@modrinth/assets'
import { Badge, injectNotificationManager, injectProjectPageContext } from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed } from 'vue'
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
import { useBaseFetch } from '~/composables/fetch.js'
import {
getProjectLink,
isApproved,
@@ -116,11 +119,13 @@ const { addNotification } = injectNotificationManager()
const { projectV2: project, currentMember, invalidate } = injectProjectPageContext()
const auth = await useAuth()
const queryClient = useQueryClient()
const { data: thread } = await useAsyncData(
() => `thread/${project.value.thread_id}`,
() => useBaseFetch(`thread/${project.value.thread_id}`),
)
const { data: thread } = useQuery({
queryKey: computed(() => ['thread', project.value?.thread_id]),
queryFn: () => useBaseFetch(`thread/${project.value.thread_id}`),
enabled: computed(() => !!project.value?.thread_id),
})
async function setStatus(status) {
startLoading()
@@ -135,7 +140,7 @@ async function setStatus(status) {
project.value.status = status
await invalidate()
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
await queryClient.invalidateQueries({ queryKey: ['thread', project.value?.thread_id] })
} catch (err) {
addNotification({
title: 'An error occurred',

View File

@@ -563,8 +563,10 @@ import {
StyledInput,
Toggle,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import ConfirmTransferProjectModal from '~/components/ui/ConfirmTransferProjectModal.vue'
import { useBaseFetch } from '~/composables/fetch.js'
import { removeSelfFromTeam } from '~/helpers/teams.js'
const { addNotification } = injectNotificationManager()
@@ -619,10 +621,13 @@ const selectedOrganizationId = ref('')
const transferData = ref(null)
const transferModal = ref(null)
const { data: organizations } = useAsyncData('organizations', () => {
return useBaseFetch('user/' + auth.value?.user.id + '/organizations', {
apiVersion: 3,
})
const { data: organizations } = useQuery({
queryKey: computed(() => ['user', auth.value?.user?.id, 'organizations']),
queryFn: () =>
useBaseFetch('user/' + auth.value?.user.id + '/organizations', {
apiVersion: 3,
}),
enabled: computed(() => !!auth.value?.user?.id),
})
const organizationOptions = computed(() =>

View File

@@ -95,6 +95,10 @@ import {
StyledInput,
} from '@modrinth/ui'
import type { AffiliateLink, User } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { useBaseFetch } from '~/composables/fetch.js'
const { handleError } = injectNotificationManager()
@@ -109,11 +113,12 @@ const revokeModal = useTemplateRef<typeof ConfirmModal>('revokeModal')
const {
data: affiliateCodes,
error,
refresh,
} = await useAsyncData(
'AffiliateLinks',
() => useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
)
refetch,
} = useQuery({
queryKey: ['affiliate'],
queryFn: () =>
useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
})
const filterQuery = ref('')
const creatingLink = ref(false)
@@ -130,16 +135,13 @@ const userIds = computed(() => {
return Array.from(ids)
})
const { data: users } = await useAsyncData(
'admin-affiliates-bulk-users',
() => {
const { data: users } = useQuery({
queryKey: computed(() => ['users-bulk', userIds.value]),
queryFn: () => {
if (userIds.value.length === 0) return Promise.resolve([])
return useBaseFetch(`users?ids=${JSON.stringify(userIds.value)}`) as Promise<User[]>
},
{
watch: [userIds],
},
)
})
const userMap = computed(() => {
if (!users.value) {
@@ -225,7 +227,7 @@ async function createAffiliateCode(data: { sourceName: string; username?: string
internal: true,
})
await refresh()
await refetch()
createModal.value?.close()
} catch (err) {
handleError(err)
@@ -255,7 +257,7 @@ async function confirmRevokeAffiliateCode() {
internal: true,
})
await refresh()
await refetch()
revokeModal.value?.hide()
revokingAffiliateUsername.value = null
revokingAffiliateId.value = null

View File

@@ -339,9 +339,11 @@ import {
} from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
import { useBaseFetch } from '~/composables/fetch.js'
const { addNotification } = injectNotificationManager()
const formatPrice = useFormatPrice()
@@ -370,9 +372,10 @@ const messages = defineMessages({
},
})
const { data: user } = await useAsyncData(`user/${route.params.id}`, () =>
useBaseFetch(`user/${route.params.id}`),
)
const { data: user } = useQuery({
queryKey: ['user', route.params.id],
queryFn: () => useBaseFetch(`user/${route.params.id}`),
})
if (!user.value) {
throw createError({
@@ -382,27 +385,25 @@ if (!user.value) {
})
}
let subscriptions, charges, refreshCharges
try {
;[{ data: subscriptions }, { data: charges, refresh: refreshCharges }] = await Promise.all([
useAsyncData(`billing/subscriptions?user_id=${route.params.id}`, () =>
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
internal: true,
}),
),
useAsyncData(`billing/payments?user_id=${route.params.id}`, () =>
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
internal: true,
}),
),
])
} catch {
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
}
const { data: subscriptions } = useQuery({
queryKey: computed(() => ['billing', 'subscriptions', user.value?.id]),
queryFn: () =>
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
internal: true,
}),
enabled: computed(() => !!user.value?.id),
placeholderData: [],
})
const { data: charges, refetch: refreshCharges } = useQuery({
queryKey: computed(() => ['billing', 'payments', user.value?.id]),
queryFn: () =>
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
internal: true,
}),
enabled: computed(() => !!user.value?.id),
placeholderData: [],
})
const subscriptionCharges = computed(() => {
return subscriptions.value.map((subscription) => {

View File

@@ -91,6 +91,8 @@ import {
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
import { useAuth } from '@/composables/auth.js'
import { useScopes } from '@/composables/auth/scopes.ts'
@@ -163,24 +165,36 @@ const getFlowIdAuthorization = async () => {
const {
data: authorizationData,
pending,
isPending: pending,
error,
} = await useAsyncData('authorization', getFlowIdAuthorization)
} = useQuery({
queryKey: computed(() => ['authorization', clientId, redirectUri, scope, state]),
queryFn: getFlowIdAuthorization,
enabled: computed(() => !!clientId && !!redirectUri && !!scope),
})
const { data: app } = await useAsyncData('oauth/app/' + clientId, () =>
useBaseFetch('oauth/app/' + clientId, {
method: 'GET',
internal: true,
}),
)
const { data: app } = useQuery({
queryKey: computed(() => ['oauth/app', clientId]),
queryFn: () =>
useBaseFetch('oauth/app/' + clientId, {
method: 'GET',
internal: true,
}),
enabled: computed(() => !!clientId),
})
const scopeDefinitions = scopesToDefinitions(BigInt(authorizationData.value?.requested_scopes || 0))
const { data: createdBy } = useQuery({
queryKey: computed(() => ['user', app.value?.created_by]),
queryFn: () =>
useBaseFetch('user/' + app.value.created_by, {
method: 'GET',
apiVersion: 3,
}),
enabled: computed(() => !!app.value?.created_by),
})
const { data: createdBy } = await useAsyncData('user/' + app.value.created_by, () =>
useBaseFetch('user/' + app.value.created_by, {
method: 'GET',
apiVersion: 3,
}),
const scopeDefinitions = computed(() =>
scopesToDefinitions(BigInt(authorizationData.value?.requested_scopes || 0)),
)
const onAuthorize = async () => {

View File

@@ -73,6 +73,7 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import HCaptcha from '@/components/ui/HCaptcha.vue'
@@ -162,13 +163,16 @@ if (route.query.flow) {
const captcha = ref()
const { data: globals } = await useAsyncData('auth-globals', async () => {
try {
return await useBaseFetch('globals', { internal: true })
} catch (err) {
console.error('Error fetching globals:', err)
return { captcha_enabled: true }
}
const { data: globals } = useQuery({
queryKey: ['auth-globals'],
queryFn: async () => {
try {
return await useBaseFetch('globals', { internal: true })
} catch (err) {
console.error('Error fetching globals:', err)
return { captcha_enabled: true }
}
},
})
const email = ref('')

View File

@@ -144,7 +144,7 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import HCaptcha from '@/components/ui/HCaptcha.vue'
import { getAuthUrl, getLauncherRedirectUrl } from '@/composables/auth.js'
@@ -207,13 +207,16 @@ if (auth.value.user) {
const captcha = ref()
const { data: globals } = await useAsyncData('auth-globals', async () => {
try {
return await useBaseFetch('globals', { internal: true })
} catch (err) {
console.error('Error fetching globals:', err)
return { captcha_enabled: true }
}
const { data: globals } = useQuery({
queryKey: ['auth-globals'],
queryFn: async () => {
try {
return await useBaseFetch('globals', { internal: true })
} catch (err) {
console.error('Error fetching globals:', err)
return { captcha_enabled: true }
}
},
})
const email = ref('')

View File

@@ -146,6 +146,7 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import HCaptcha from '@/components/ui/HCaptcha.vue'
import { getAuthUrl } from '@/composables/auth.js'
@@ -200,13 +201,16 @@ if (auth.value.user) {
const captcha = ref()
const { data: globals } = await useAsyncData('auth-globals', async () => {
try {
return await useBaseFetch('globals', { internal: true })
} catch (err) {
console.error('Error fetching globals:', err)
return { captcha_enabled: true }
}
const { data: globals } = useQuery({
queryKey: ['auth-globals'],
queryFn: async () => {
try {
return await useBaseFetch('globals', { internal: true })
} catch (err) {
console.error('Error fetching globals:', err)
return { captcha_enabled: true }
}
},
})
const email = ref('')

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import type { AffiliateLink } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
const createModal = useTemplateRef<typeof AffiliateLinkCreateModal>('createModal')
const revokeModal = useTemplateRef<typeof ConfirmModal>('revokeModal')
@@ -83,11 +84,12 @@ const { formatMessage } = useVIntl()
const {
data: affiliateLinks,
error,
refresh,
} = await useAsyncData(
'affiliateLinks',
() => useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
)
refetch,
} = useQuery({
queryKey: ['affiliate'],
queryFn: () =>
useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
})
const filterQuery = ref('')
const creatingLink = ref(false)
@@ -116,7 +118,7 @@ async function createAffiliateCode(data: { sourceName: string }) {
internal: true,
})
await refresh()
await refetch()
createModal.value?.close()
} catch (err) {
handleError(err)
@@ -145,7 +147,7 @@ async function confirmRevokeAffiliateLink() {
internal: true,
})
await refresh()
await refetch()
revokeModal.value?.hide()
revokingTitle.value = null
revokingId.value = null

View File

@@ -5,6 +5,8 @@
</template>
<script setup>
import { useQuery } from '@tanstack/vue-query'
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
definePageMeta({
@@ -18,7 +20,9 @@ useHead({
const auth = await useAuth()
const id = auth.value?.user?.id
const { data: projects } = await useAsyncData(`user/${id}/projects`, () =>
useBaseFetch(`user/${id}/projects`),
)
const { data: projects } = useQuery({
queryKey: computed(() => ['user', id, 'projects']),
queryFn: () => useBaseFetch(`user/${id}/projects`),
enabled: computed(() => !!id),
})
</script>

View File

@@ -159,8 +159,10 @@ import {
useCompactNumber,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import { useBaseFetch } from '~/composables/fetch.js'
const { formatMessage } = useVIntl()
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
@@ -221,9 +223,10 @@ if (import.meta.client) {
const filterQuery = ref('')
const { data: collections } = await useAsyncData(`user/${auth.value.user.id}/collections`, () =>
useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
)
const { data: collections } = useQuery({
queryKey: ['user', auth.value.user.id, 'collections'],
queryFn: () => useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
})
const route = useNativeRoute()
const router = useNativeRouter()

View File

@@ -35,7 +35,7 @@
:auth="auth"
raised
compact
@update:notifications="() => refresh()"
@update:notifications="() => refetch()"
/>
<nuxt-link
v-if="extraNotifs > 0"
@@ -98,6 +98,7 @@
<script setup>
import { ChevronRightIcon, HistoryIcon } from '@modrinth/assets'
import { Avatar } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import NotificationItem from '~/components/ui/NotificationItem.vue'
import { fetchExtraNotificationData, groupNotifications } from '~/helpers/platform-notifications.ts'
@@ -108,11 +109,11 @@ useHead({
const auth = await useAuth()
const [{ data: projects }] = await Promise.all([
useAsyncData(`user/${auth.value.user.id}/projects`, () =>
useBaseFetch(`user/${auth.value.user.id}/projects`),
),
])
const { data: projects } = useQuery({
queryKey: computed(() => ['user', auth.value?.user?.id, 'projects']),
queryFn: async () => await useBaseFetch(`user/${auth.value?.user?.id}/projects`),
placeholderData: [],
})
const downloadsProjectCount = computed(
() => projects.value.filter((project) => project.downloads > 0).length,
@@ -121,23 +122,24 @@ const followersProjectCount = computed(
() => projects.value.filter((project) => project.followers > 0).length,
)
const { data, refresh } = await useAsyncData(async () => {
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`)
const { data, refetch } = useQuery({
queryKey: computed(() => ['user', auth.value?.user?.id, 'notifications']),
queryFn: async () => {
const notifications = await useBaseFetch(`user/${auth.value?.user?.id}/notifications`)
const filteredNotifications = notifications.filter((notif) => !notif.read)
const slice = filteredNotifications.slice(0, 30) // send first 30 notifs to be grouped before trimming to 3
const filteredNotifications = notifications.filter((notif) => !notif.read)
const slice = filteredNotifications.slice(0, 30)
return fetchExtraNotificationData(slice).then((notifications) => {
notifications = groupNotifications(notifications).slice(0, 3)
return { notifications, extraNotifs: filteredNotifications.length - slice.length }
})
return fetchExtraNotificationData(slice).then((notifications) => {
notifications = groupNotifications(notifications).slice(0, 3)
return { notifications, extraNotifs: filteredNotifications.length - slice.length }
})
},
enabled: computed(() => !!auth.value?.user?.id),
})
const notifications = computed(() => {
if (data.value === null) {
return []
}
return data.value.notifications
return data.value?.notifications ?? []
})
const extraNotifs = computed(() => (data.value ? data.value.extraNotifs : 0))

View File

@@ -29,7 +29,7 @@
:format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')"
:capitalize="false"
/>
<p v-if="pending">Loading notifications...</p>
<p v-if="isPending">Loading notifications...</p>
<template v-else-if="error">
<p>Error loading notifications:</p>
<pre>
@@ -45,7 +45,7 @@
:notification="notification"
:auth="auth"
raised
@update:notifications="() => refresh()"
@update:notifications="() => refetch()"
/>
</template>
<p v-else>You don't have any unread notifications.</p>
@@ -59,6 +59,7 @@
import { CheckCheckIcon, HistoryIcon } from '@modrinth/assets'
import { Button, Chips, Pagination } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import NotificationItem from '~/components/ui/NotificationItem.vue'
@@ -81,11 +82,19 @@ const selectedType = ref('all')
const page = ref(1)
const perPage = ref(50)
const { data, pending, error, refresh } = await useAsyncData(
async () => {
const { data, isPending, error, refetch } = useQuery({
queryKey: computed(() => [
'user',
auth.value?.user?.id,
'notifications',
page.value,
history.value,
selectedType.value,
]),
queryFn: async () => {
const pageNum = page.value - 1
const showRead = history.value
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`)
const notifications = await useBaseFetch(`user/${auth.value?.user?.id}/notifications`)
const typesInFeed = [
...new Set(notifications.filter((n) => showRead || !n.read).map((n) => n.type)),
@@ -107,8 +116,9 @@ const { data, pending, error, refresh } = await useAsyncData(
hasRead: notifications.some((n) => n.read),
}))
},
{ watch: [page, history, selectedType] },
)
enabled: computed(() => !!auth.value?.user?.id),
placeholderData: { notifications: [], notifTypes: [], pages: 1, hasRead: false },
})
const notifications = computed(() =>
data.value ? groupNotifications(data.value.notifications, history.value) : [],
@@ -130,7 +140,7 @@ async function readAll() {
])
await markAsRead(ids)
await refresh()
await refetch()
}
function changePage(newPage) {

View File

@@ -51,6 +51,7 @@
<script setup>
import { PlusIcon, UsersIcon } from '@modrinth/assets'
import { Avatar } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
import { useAuth } from '~/composables/auth.js'
@@ -60,12 +61,13 @@ const createOrgModal = ref(null)
const auth = await useAuth()
const uid = computed(() => auth.value.user?.id || null)
const { data: orgs, error } = useAsyncData('organizations', () => {
if (!uid.value) return Promise.resolve(null)
return useBaseFetch('user/' + uid.value + '/organizations', {
apiVersion: 3,
})
const { data: orgs, error } = useQuery({
queryKey: computed(() => ['user', uid.value, 'organizations']),
queryFn: () =>
useBaseFetch('user/' + uid.value + '/organizations', {
apiVersion: 3,
}),
enabled: computed(() => !!uid.value),
})
const sortedOrgs = computed(() =>

View File

@@ -261,6 +261,7 @@
<script setup lang="ts">
import { ArrowUpRightIcon, InProgressIcon, UnknownIcon } from '@modrinth/assets'
import { defineMessages, useFormatDateTime, useFormatMoney, useVIntl } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'
@@ -356,9 +357,9 @@ const messages = defineMessages({
},
})
const { data: userBalance, refresh: refreshUserBalance } = await useAsyncData(
`payout/balance`,
async () => {
const { data: userBalance, refetch: refreshUserBalance } = useQuery({
queryKey: ['payout', 'balance'],
queryFn: async () => {
const response = (await useBaseFetch(`payout/balance`, {
apiVersion: 3,
})) as UserBalanceResponse
@@ -370,28 +371,33 @@ const { data: userBalance, refresh: refreshUserBalance } = await useAsyncData(
pending: Number(response.pending),
}
},
)
})
const { data: payouts, refresh: refreshPayouts } = await useAsyncData(`payout/history`, () =>
useBaseFetch(`payout/history`, {
apiVersion: 3,
}),
)
const { data: payouts, refetch: refreshPayouts } = useQuery({
queryKey: ['payout', 'history'],
queryFn: () =>
useBaseFetch(`payout/history`, {
apiVersion: 3,
}),
})
const userCountry = useUserCountry()
const { data: preloadedPaymentMethods } = await useAsyncData(`payout/methods-preload`, async () => {
const defaultCountry = userCountry.value || 'US'
try {
return {
country: defaultCountry,
methods: (await useBaseFetch('payout/methods', {
apiVersion: 3,
query: { country: defaultCountry },
})) as PayoutMethod[],
const { data: preloadedPaymentMethods } = useQuery({
queryKey: computed(() => ['payout', 'methods-preload', userCountry.value]),
queryFn: async () => {
const defaultCountry = userCountry.value || 'US'
try {
return {
country: defaultCountry,
methods: (await useBaseFetch('payout/methods', {
apiVersion: 3,
query: { country: defaultCountry },
})) as PayoutMethod[],
}
} catch {
return null
}
} catch {
return null
}
},
})
const sortedPayouts = computed(() => {

View File

@@ -67,7 +67,7 @@
v-for="transaction in transactions"
:key="transaction.id || transaction.created"
:transaction="transaction"
@cancelled="refresh"
@cancelled="refetch"
/>
</div>
</div>
@@ -97,6 +97,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue'
@@ -116,11 +117,13 @@ useHead({
title: 'Transaction history - Modrinth',
})
const { data: transactions, refresh } = await useAsyncData(`payout-history`, () =>
useBaseFetch(`payout/history`, {
apiVersion: 3,
}),
)
const { data: transactions, refetch } = useQuery({
queryKey: ['payout', 'history'],
queryFn: () =>
useBaseFetch(`payout/history`, {
apiVersion: 3,
}),
})
const allTransactions = computed(() => {
if (!transactions.value) return []

View File

@@ -648,6 +648,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import { monthsInInterval } from '@modrinth/ui/src/utils/billing.ts'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
import { useBaseFetch } from '@/composables/fetch.js'
@@ -1011,14 +1012,18 @@ const selectedCurrency = ref('USD')
const loggedOut = computed(() => !auth.value.user)
const outOfStockUrl = 'https://discord.modrinth.com'
const { data: hasServers } = await useAsyncData('ServerListCountCheck', async () => {
try {
if (!auth.value.user) return false
const response = await useServersFetch('servers')
return response.servers && response.servers.length > 0
} catch {
return false
}
const { data: hasServers } = useQuery({
queryKey: computed(() => ['servers', 'list-count', auth.value?.user?.id]),
queryFn: async () => {
try {
if (!auth.value.user) return false
const response = await useServersFetch('servers')
return response.servers && response.servers.length > 0
} catch {
return false
}
},
enabled: computed(() => !!auth.value?.user),
})
function fetchStock(region, request) {
@@ -1080,15 +1085,12 @@ async function fetchCapacityStatuses(customProduct = null) {
}
}
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
'ServerCapacityAll',
fetchCapacityStatuses,
{
getCachedData() {
return null // Dont cache stock data.
},
},
)
const { data: capacityStatuses, refetch: refreshCapacity } = useQuery({
queryKey: ['server', 'capacity', 'all'],
queryFn: fetchCapacityStatuses,
staleTime: 0, // Dont cache stock data
gcTime: 0,
})
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0)
const isMediumAtCapacity = computed(() => capacityStatuses.value?.medium?.available === 0)

View File

@@ -118,7 +118,7 @@ import {
StyledInput,
Toggle,
} from '@modrinth/ui'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import Fuse from 'fuse.js'
import { computed, ref, watch } from 'vue'

View File

@@ -163,6 +163,7 @@
<script lang="ts" setup>
import { StyledInput, useFormatDateTime, useFormatMoney } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
@@ -188,11 +189,13 @@ const selectedDate = computed(() => dayjs(rawSelectedDate.value))
const endOfMonthDate = computed(() => selectedDate.value.endOf('month'))
const withdrawalDate = computed(() => endOfMonthDate.value.add(60, 'days'))
const { data: transparencyInformation } = await useAsyncData('payout/platform_revenue', () =>
useBaseFetch('payout/platform_revenue', {
apiVersion: 3,
}),
)
const { data: transparencyInformation } = useQuery({
queryKey: ['payout', 'platform_revenue'],
queryFn: () =>
useBaseFetch('payout/platform_revenue', {
apiVersion: 3,
}),
})
const platformRevenue = (transparencyInformation.value as any)?.all_time
const platformRevenueData = (transparencyInformation.value as any)?.data?.slice(0, 5) ?? []

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { Report } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import ModerationReportCard from '~/components/ui/moderation/ModerationReportCard.vue'
import { enrichReportBatch } from '~/helpers/moderation.ts'
@@ -7,18 +8,21 @@ import { enrichReportBatch } from '~/helpers/moderation.ts'
const { params } = useRoute()
const reportId = params.id as string
const { data: report } = await useAsyncData(`moderation-report-${reportId}`, async () => {
try {
const report = (await useBaseFetch(`report/${reportId}`, { apiVersion: 3 })) as Report
const enrichedReport = (await enrichReportBatch([report]))[0]
return enrichedReport
} catch (error) {
console.error('Error fetching report:', error)
throw createError({
statusCode: 404,
statusMessage: 'Report not found',
})
}
const { data: report } = useQuery({
queryKey: computed(() => ['report', reportId]),
queryFn: async () => {
try {
const report = (await useBaseFetch(`report/${reportId}`, { apiVersion: 3 })) as Report
const enrichedReport = (await enrichReportBatch([report]))[0]
return enrichedReport
} catch (error) {
console.error('Error fetching report:', error)
throw createError({
statusCode: 404,
statusMessage: 'Report not found',
})
}
},
})
</script>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { GitGraphIcon, RssIcon } from '@modrinth/assets'
import { articles as rawArticles } from '@modrinth/blog'
import { Avatar, ButtonStyled, useFormatDateTime } from '@modrinth/ui'
import type { User } from '@modrinth/utils'
import { Avatar, ButtonStyled, injectModrinthClient, useFormatDateTime } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import { computed, onMounted } from 'vue'
import NewsletterButton from '~/components/ui/NewsletterButton.vue'
import ShareArticleButtons from '~/components/ui/ShareArticleButtons.vue'
const client = injectModrinthClient()
const config = useRuntimeConfig()
const route = useRoute()
@@ -24,21 +25,19 @@ if (!rawArticle) {
})
}
const authorsUrl = `users?ids=${JSON.stringify(rawArticle.authors)}`
const { data: authors } = useQuery({
queryKey: computed(() => ['users', rawArticle.authors]),
queryFn: async () => {
const users = await client.labrinth.users_v2.getMultiple(rawArticle.authors)
users.sort((a, b) => {
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id)
})
return users
},
enabled: computed(() => rawArticle.authors.length > 0),
})
const [authors, html] = await Promise.all([
rawArticle.authors
? useAsyncData(authorsUrl, () => useBaseFetch(authorsUrl)).then((data) => {
const users = data.data as Ref<User[]>
users.value.sort((a, b) => {
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id)
})
return users
})
: Promise.resolve(),
rawArticle.html(),
])
const html = await rawArticle.html()
const article = computed(() => ({
...rawArticle,

View File

@@ -1,6 +1,9 @@
<template>
<div v-if="isLoading" class="flex min-h-[50vh] items-center justify-center">
<SpinnerIcon class="h-12 w-12 animate-spin text-brand" />
</div>
<div
v-if="organization"
v-else-if="organization"
class="experimental-styles-within new-page sidebar"
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }"
>
@@ -240,10 +243,14 @@
:downloads="project.downloads"
:followers="project.followers"
:tags="project.categories"
:environment="{
clientSide: project.client_side,
serverSide: project.server_side,
}"
:environment="
project.client_side && project.server_side
? {
clientSide: project.client_side,
serverSide: project.server_side,
}
: undefined
"
:status="
auth.user && (auth.user.id! === user.id || tags.staffRoles.includes(auth.user.role))
? (project.status as ProjectStatus)
@@ -282,6 +289,7 @@ import {
MoreVerticalIcon,
OrganizationIcon,
SettingsIcon,
SpinnerIcon,
UsersIcon,
XIcon,
} from '@modrinth/assets'
@@ -290,6 +298,7 @@ import {
ButtonStyled,
commonMessages,
ContentPageHeader,
injectModrinthClient,
OverflowMenu,
ProjectCard,
ProjectCardList,
@@ -298,6 +307,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
@@ -342,70 +352,95 @@ if (route.path.includes('settings')) {
// hacky way to show the edit button on the corner of the card.
const routeHasSettings = computed(() => route.path.includes('settings'))
const [
{ data: organization, refresh: refreshOrganization },
{ data: projects, refresh: refreshProjects },
] = await Promise.all([
useAsyncData(
`organization/${orgId}`,
() => useBaseFetch(`organization/${orgId}`, { apiVersion: 3 }) as Promise<Organization>,
),
useAsyncData(
`organization/${orgId}/projects`,
() => useBaseFetch(`organization/${orgId}/projects`, { apiVersion: 3 }) as Promise<ProjectV3[]>,
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
const client = injectModrinthClient()
if (project.mrpack_loaders) {
project.categories = project.categories.concat(project.mrpack_loaders)
}
const {
data: organization,
refetch: refreshOrganization,
error: orgError,
isPending: organizationIsPending,
} = useQuery({
queryKey: computed(() => ['organization', orgId]),
// @ts-expect-error
queryFn: () => client.labrinth.organizations_v3.get(orgId),
enabled: !!orgId,
})
const singleplayer = project.singleplayer && project.singleplayer[0]
const clientAndServer = project.client_and_server && project.client_and_server[0]
const clientOnly = project.client_only && project.client_only[0]
const serverOnly = project.server_only && project.server_only[0]
watch(
orgError,
(error) => {
if (error) {
const status = (error as any).statusCode ?? (error as any).status ?? 404
showError({
fatal: true,
statusCode: status,
message: 'Organization not found',
})
}
},
{ immediate: true },
)
// quick and dirty hack to show envs as legacy
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
project.client_side = 'required'
project.server_side = 'required'
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
project.client_side = 'required'
project.server_side = 'unsupported'
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
project.client_side = 'unsupported'
project.server_side = 'required'
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
project.client_side = 'optional'
project.server_side = 'optional'
}
}
const {
data: projects,
refetch: refreshProjects,
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 projects
},
},
),
])
return rawProjects.map((project) => {
let categories = project.categories.concat(project.loaders)
if (project.mrpack_loaders) {
categories = categories.concat(project.mrpack_loaders as string[])
}
const singleplayer = project.singleplayer && (project.singleplayer as string[])[0]
const clientAndServer =
project.client_and_server && (project.client_and_server as string[])[0]
const clientOnly = project.client_only && (project.client_only as string[])[0]
const serverOnly = project.server_only && (project.server_only as string[])[0]
let client_side: ProjectV3['client_side'] | undefined
let server_side: ProjectV3['server_side'] | undefined
// quick and dirty hack to show envs as legacy
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
client_side = 'required'
server_side = 'required'
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
client_side = 'required'
server_side = 'unsupported'
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
client_side = 'unsupported'
server_side = 'required'
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
client_side = 'optional'
server_side = 'optional'
}
return { ...project, categories, client_side, server_side }
})
},
placeholderData: [],
})
const refresh = async () => {
await Promise.all([refreshOrganization(), refreshProjects()])
}
if (!organization.value) {
throw createError({
fatal: true,
statusCode: 404,
message: 'Organization not found',
})
}
// Loading state
const isLoading = computed(() => {
return organizationIsPending.value || projectsIsFetching.value
})
// Filter accepted, sort by role, then by name and Owner role always goes first
const acceptedMembers = computed(() => {
const acceptedMembers = organization.value?.members?.filter((x) => x.accepted) ?? []
const owner = acceptedMembers.find((x) => x.is_owner)
const rest = acceptedMembers.filter((x) => !x.is_owner) || []
const rest = acceptedMembers.filter((x) => !x.is_owner) ?? []
rest.sort((a, b) => {
if (a.role === b.role) {
@@ -415,7 +450,7 @@ const acceptedMembers = computed(() => {
}
})
return [owner, ...rest]
return owner ? [owner, ...rest] : rest
})
const isInvited = computed(() => {
@@ -477,21 +512,35 @@ const onDeclineInvite = useClientTry(async () => {
await refreshOrganization()
})
const organizationContext = new OrganizationContext(organization, projects, auth, tags, refresh)
const organizationContext = new OrganizationContext(
organization as Ref<Organization | null>,
projects as Ref<ProjectV3[] | null>,
auth,
tags,
refresh,
)
const { currentMember } = organizationContext
provideOrganizationContext(organizationContext)
const title = `${organization.value.name} - Organization`
const description = `${organization.value.description} - View the organization ${organization.value.name} on Modrinth`
watch(
organization,
(org) => {
if (org) {
const title = `${org.name} - Organization`
const description = `${org.description} - View the organization ${org.name} on Modrinth`
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: organization.value.description,
ogImage: organization.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
})
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: org.description,
ogImage: org.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
})
}
},
{ immediate: true },
)
const navLinks = computed(() => [
{

View File

@@ -334,6 +334,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue'
@@ -347,13 +348,12 @@ const { organization, projects, refresh } = injectOrganizationContext()
const auth = await useAuth()
const { data: userProjects, refresh: refreshUserProjects } = await useAsyncData(
`user/${auth.value.user.id}/projects`,
() => useBaseFetch(`user/${auth.value.user.id}/projects`),
{
watch: [auth],
},
)
const { data: userProjects, refetch: refreshUserProjects } = useQuery({
queryKey: computed(() => ['user', auth.value?.user?.id, 'projects']),
queryFn: () => useBaseFetch(`user/${auth.value.user.id}/projects`),
enabled: computed(() => !!auth.value?.user?.id),
placeholderData: [],
})
const usersOwnedProjects = ref([])

View File

@@ -260,6 +260,7 @@ import {
useFormatDateTime,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import Modal from '~/components/ui/Modal.vue'
import {
@@ -491,16 +492,14 @@ const loading = ref(false)
const auth = await useAuth()
const { data: usersApps, refresh } = await useAsyncData(
'usersApps',
() =>
const { data: usersApps, refetch: refresh } = useQuery({
queryKey: computed(() => ['user', auth.value?.user?.id, 'oauth_apps']),
queryFn: () =>
useBaseFetch(`user/${auth.value.user.id}/oauth_apps`, {
apiVersion: 3,
}),
{
watch: [auth],
},
)
enabled: computed(() => !!auth.value?.user?.id),
})
const setForm = (app) => {
if (app?.id) {

View File

@@ -98,6 +98,7 @@ import {
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { useScopes } from '~/composables/auth/scopes.ts'
@@ -116,42 +117,36 @@ useHead({
title: 'Authorizations - Modrinth',
})
const { data: usersApps, refresh } = await useAsyncData('userAuthorizations', () =>
useBaseFetch(`oauth/authorizations`, {
internal: true,
}),
)
const { data: usersApps, refetch: refresh } = useQuery({
queryKey: ['oauth', 'authorizations'],
queryFn: () =>
useBaseFetch(`oauth/authorizations`, {
internal: true,
}),
})
const { data: appInformation } = await useAsyncData(
'appInfo',
() => {
if (!usersApps.value?.length) return null
return useBaseFetch('oauth/apps', {
const { data: appInformation } = useQuery({
queryKey: computed(() => ['oauth', 'apps', usersApps.value?.map((c) => c.app_id)]),
queryFn: () =>
useBaseFetch('oauth/apps', {
internal: true,
query: {
ids: JSON.stringify(usersApps.value.map((c) => c.app_id)),
},
})
},
{
watch: usersApps,
},
)
}),
enabled: computed(() => !!usersApps.value?.length),
})
const { data: appCreatorsInformation } = await useAsyncData(
'appCreatorsInfo',
() => {
if (!appInformation.value?.length) return null
return useBaseFetch('users', {
const { data: appCreatorsInformation } = useQuery({
queryKey: computed(() => ['users', appInformation.value?.map((c) => c.created_by)]),
queryFn: () =>
useBaseFetch('users', {
query: {
ids: JSON.stringify(appInformation.value.map((c) => c.created_by)),
},
})
},
{
watch: appInformation,
},
)
}),
enabled: computed(() => !!appInformation.value?.length),
})
const appInfoLookup = computed(() => {
if (!usersApps.value || !appInformation.value || !appCreatorsInformation.value) {

View File

@@ -39,6 +39,7 @@
</template>
<script setup>
import { Badge, Breadcrumbs, useFormatDateTime, useFormatPrice } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { products } from '~/generated/state.json'
@@ -53,23 +54,22 @@ const formatDate = useFormatDateTime({
day: '2-digit',
})
const { data: charges } = await useAsyncData(
'billing/payments',
() => useBaseFetch('billing/payments', { internal: true }),
{
transform: (charges) => {
return charges
.filter((charge) => charge.status !== 'open' && charge.status !== 'cancelled')
.map((charge) => {
const product = products.find((product) =>
product.prices.some((price) => price.id === charge.price_id),
)
const { data: charges } = useQuery({
queryKey: ['billing', 'payments'],
queryFn: async () => {
const charges = await useBaseFetch('billing/payments', { internal: true })
return charges
.filter((charge) => charge.status !== 'open' && charge.status !== 'cancelled')
.map((charge) => {
const product = products.find((product) =>
product.prices.some((price) => price.id === charge.price_id),
)
charge.product = product
charge.product = product
return charge
})
},
return charge
})
},
)
placeholderData: [],
})
</script>

View File

@@ -299,12 +299,14 @@
<div class="flex text-2xl font-bold text-contrast">
<span class="text-contrast">
{{
formatPrice(
getProductPrice(getPyroProduct(subscription), subscription.interval)
.prices.intervals[subscription.interval],
getProductPrice(getPyroProduct(subscription), subscription.interval)
.currency_code,
)
getProductPrice(getPyroProduct(subscription), subscription.interval)
? formatPrice(
getProductPrice(getPyroProduct(subscription), subscription.interval)
.prices.intervals[subscription.interval],
getProductPrice(getPyroProduct(subscription), subscription.interval)
.currency_code,
)
: ''
}}
</span>
<span>/{{ subscription.interval.replace('ly', '') }}</span>
@@ -450,6 +452,7 @@
@proceed="removePaymentMethod(removePaymentMethodIndex)"
/>
<PurchaseModal
v-if="customer && paymentMethods"
ref="midasPurchaseModal"
:product="midasProduct"
:country="country"
@@ -620,6 +623,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import { calculateSavings, getCurrency } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { useBaseFetch } from '@/composables/fetch.js'
@@ -734,25 +738,32 @@ const messages = defineMessages({
},
})
const [
{ data: paymentMethods, refresh: refreshPaymentMethods },
{ data: charges, refresh: refreshCharges },
{ data: customer, refresh: refreshCustomer },
{ data: subscriptions, refresh: refreshSubscriptions },
{ data: productsData, refresh: refreshProducts },
{ data: serversData, refresh: refreshServers },
] = await Promise.all([
useAsyncData('billing/payment_methods', () =>
useBaseFetch('billing/payment_methods', { internal: true }),
),
useAsyncData('billing/payments', () => useBaseFetch('billing/payments', { internal: true })),
useAsyncData('billing/customer', () => useBaseFetch('billing/customer', { internal: true })),
useAsyncData('billing/subscriptions', () =>
useBaseFetch('billing/subscriptions', { internal: true }),
),
useAsyncData('billing/products', () => useBaseFetch('billing/products', { internal: true })),
useAsyncData('servers', () => useServersFetch('servers')),
])
const queryClient = useQueryClient()
const { data: paymentMethods } = useQuery({
queryKey: ['billing', 'payment_methods'],
queryFn: () => useBaseFetch('billing/payment_methods', { internal: true }),
})
const { data: charges } = useQuery({
queryKey: ['billing', 'payments'],
queryFn: () => useBaseFetch('billing/payments', { internal: true }),
})
const { data: customer } = useQuery({
queryKey: ['billing', 'customer'],
queryFn: () => useBaseFetch('billing/customer', { internal: true }),
})
const { data: subscriptions } = useQuery({
queryKey: ['billing', 'subscriptions'],
queryFn: () => useBaseFetch('billing/subscriptions', { internal: true }),
})
const { data: productsData } = useQuery({
queryKey: ['billing', 'products'],
queryFn: () => useBaseFetch('billing/products', { internal: true }),
})
const { data: serversData } = useQuery({
queryKey: ['servers'],
queryFn: () => useServersFetch('servers'),
})
const midasProduct = ref(products.find((x) => x.metadata?.type === 'midas'))
const midasSubscription = computed(() =>
@@ -996,12 +1007,8 @@ const resubscribePyro = async (subscriptionId, wasSuspended) => {
const refresh = async () => {
await Promise.all([
refreshPaymentMethods(),
refreshCharges(),
refreshCustomer(),
refreshSubscriptions(),
refreshProducts(),
refreshServers(),
queryClient.invalidateQueries({ queryKey: ['billing'] }),
queryClient.invalidateQueries({ queryKey: ['servers'] }),
])
}

View File

@@ -203,6 +203,7 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import Modal from '~/components/ui/Modal.vue'
import {
@@ -326,7 +327,11 @@ const deletePatIndex = ref(null)
const loading = ref(false)
const { data: pats, refresh } = await useAsyncData('pat', () => useBaseFetch('pat'))
const { data: pats, refetch: refresh } = useQuery({
queryKey: ['pat'],
queryFn: () => useBaseFetch('pat'),
placeholderData: [],
})
const displayPats = computed(() => {
return pats.value.toSorted((a, b) => new Date(b.created) - new Date(a.created))
})

View File

@@ -52,6 +52,7 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
definePageMeta({
middleware: 'auth',
@@ -101,9 +102,10 @@ useHead({
title: () => `${formatMessage(commonSettingsMessages.sessions)} - Modrinth`,
})
const { data: sessions, refresh } = await useAsyncData('session/list', () =>
useBaseFetch('session/list'),
)
const { data: sessions, refetch: refresh } = useQuery({
queryKey: ['session', 'list'],
queryFn: () => useBaseFetch('session/list'),
})
async function revokeSession(id) {
startLoading()

View File

@@ -282,7 +282,7 @@
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
<NavTabs :links="navLinks" replace />
</div>
<div v-if="projects.length > 0">
<div v-if="projects?.length > 0">
<ProjectCardList
v-if="route.params.projectType !== 'collections'"
:layout="cosmetics.searchDisplayMode.user"
@@ -331,7 +331,7 @@
<div
v-else-if="
(route.params.projectType && route.params.projectType !== 'collections') ||
(!route.params.projectType && collections.length === 0)
(!route.params.projectType && collections?.length === 0)
"
class="error"
>
@@ -353,7 +353,7 @@
class="collections-grid"
>
<nuxt-link
v-for="collection in collections.sort(
v-for="collection in (collections ?? []).sort(
(a, b) => new Date(b.created) - new Date(a.created),
)"
:key="collection.id"
@@ -404,7 +404,7 @@
</nuxt-link>
</div>
<div
v-if="route.params.projectType === 'collections' && collections.length === 0"
v-if="route.params.projectType === 'collections' && collections?.length === 0"
class="error"
>
<UpToDate class="icon" />
@@ -425,7 +425,7 @@
</div>
</div>
<div class="normal-page__sidebar">
<div v-if="organizations.length > 0" class="card flex-card">
<div v-if="organizations?.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">
{{ formatMessage(messages.profileOrganizations) }}
</h2>
@@ -492,6 +492,7 @@ import {
commonMessages,
ContentPageHeader,
defineMessages,
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
NewModal,
@@ -506,6 +507,8 @@ import {
useVIntl,
} from '@modrinth/ui'
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { onServerPrefetch } from 'vue'
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
import AlphaTesterBadge from '~/assets/images/badges/alpha-tester.svg?component'
@@ -527,6 +530,7 @@ const auth = await useAuth()
const cosmetics = useCosmetics()
const tags = useGeneratedState()
const config = useRuntimeConfig()
const queryClient = useQueryClient()
const { formatMessage } = useVIntl()
const formatNumber = useFormatNumber()
@@ -679,74 +683,81 @@ const messages = defineMessages({
},
})
let user, projects, organizations, collections, refreshUser
try {
;[
{ data: user, refresh: refreshUser },
{ data: projects },
{ data: organizations },
{ data: collections },
] = await Promise.all([
useAsyncData(`user/${route.params.id}`, () => useBaseFetch(`user/${route.params.id}`)),
useAsyncData(
`user/${route.params.id}/projects`,
() => useBaseFetch(`user/${route.params.id}/projects`),
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.categories,
tags.value,
)
}
const client = injectModrinthClient()
return projects
},
},
),
useAsyncData(`user/${route.params.id}/organizations`, () =>
useBaseFetch(`user/${route.params.id}/organizations`, {
apiVersion: 3,
}),
),
useAsyncData(`user/${route.params.id}/collections`, () =>
useBaseFetch(`user/${route.params.id}/collections`, { apiVersion: 3 }),
),
const {
data: user,
error: userError,
suspense: userSuspense,
} = useQuery({
queryKey: computed(() => ['user', route.params.id]),
queryFn: () => client.labrinth.users_v2.get(route.params.id),
})
watch(
userError,
(error) => {
if (error) {
const status = error.statusCode ?? error.status ?? 404
showError({
fatal: true,
statusCode: status,
message: formatMessage(messages.userNotFoundError),
})
}
},
{ immediate: true },
)
const { data: projects, suspense: projectsSuspense } = useQuery({
queryKey: computed(() => ['user', route.params.id, 'projects']),
queryFn: async () => {
const projects = await client.labrinth.users_v2.getProjects(route.params.id)
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.categories,
tags.value,
)
}
return projects
},
})
const { data: organizations, suspense: orgsSuspense } = useQuery({
queryKey: computed(() => ['user', route.params.id, 'organizations']),
queryFn: () => client.labrinth.users_v2.getOrganizations(route.params.id),
})
const { data: collections, suspense: collectionsSuspense } = useQuery({
queryKey: computed(() => ['user', route.params.id, 'collections']),
queryFn: () => client.labrinth.users_v2.getCollections(route.params.id),
})
onServerPrefetch(async () => {
await Promise.allSettled([
userSuspense(),
projectsSuspense(),
orgsSuspense(),
collectionsSuspense(),
])
} catch {
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
}
})
const sortedOrgs = computed(() =>
organizations.value ? [...organizations.value].sort((a, b) => a.name.localeCompare(b.name)) : [],
)
if (!user.value) {
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
}
if (user.value.username !== route.params.id) {
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 })
}
const title = computed(() => `${user.value.username} - Modrinth`)
const title = computed(() => (user.value ? `${user.value.username} - Modrinth` : 'Modrinth'))
const description = computed(() =>
user.value.bio
user.value?.bio
? formatMessage(messages.profileMetaDescriptionWithBio, {
bio: user.value.bio,
username: user.value.username,
})
: formatMessage(messages.profileMetaDescription, { username: user.value.username }),
: user.value
? formatMessage(messages.profileMetaDescription, { username: user.value.username })
: '',
)
useSeoMeta({
@@ -754,7 +765,7 @@ useSeoMeta({
description: () => description.value,
ogTitle: () => title.value,
ogDescription: () => description.value,
ogImage: () => user.value.avatar_url ?? 'https://cdn.modrinth.com/placeholder.png',
ogImage: () => user.value?.avatar_url ?? 'https://cdn.modrinth.com/placeholder.png',
})
const projectTypes = computed(() => {
@@ -838,15 +849,12 @@ async function copyPermalink() {
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
}
const isAffiliate = computed(() => user.value.badges & UserBadge.AFFILIATE)
const isAffiliate = computed(() => user.value?.badges & UserBadge.AFFILIATE)
const isAdminViewing = computed(() => isAdmin(auth.value.user))
async function toggleAffiliate(id) {
await useBaseFetch(`user/${id}`, {
method: 'PATCH',
body: { badges: user.value.badges ^ (1 << 7) },
})
refreshUser()
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
queryClient.invalidateQueries({ queryKey: ['user', route.params.id] })
}
const navLinks = computed(() => [
@@ -865,7 +873,7 @@ const navLinks = computed(() => [
.sort((a, b) => a.label.localeCompare(b.label)),
])
const selectedRole = ref(user.value.role)
const selectedRole = ref(user.value?.role)
const isSavingRole = ref(false)
const roleOptions = [
@@ -893,12 +901,8 @@ function saveRoleEdit() {
isSavingRole.value = true
useBaseFetch(`user/${user.value.id}`, {
method: 'PATCH',
body: {
role: selectedRole.value,
},
})
client.labrinth.users_v2
.patch(user.value.id, { role: selectedRole.value })
.then(() => {
user.value.role = selectedRole.value