From fd9aa8626b566f3f09d0cc92906a7f2f656f327c Mon Sep 17 00:00:00 2001 From: tdgao Date: Sat, 31 Jan 2026 21:37:40 -0700 Subject: [PATCH 1/9] refactor: most places with useAsyncData replaced with tanstack query --- .../components/ui/create/CreateLimitAlert.vue | 11 ++-- .../ui/servers/ServerInstallation.vue | 35 +++++----- apps/frontend/src/layouts/default.vue | 8 ++- .../src/pages/[type]/[id]/moderation.vue | 15 +++-- .../pages/[type]/[id]/settings/members.vue | 13 ++-- apps/frontend/src/pages/admin/affiliates.vue | 30 +++++---- .../frontend/src/pages/admin/billing/[id].vue | 49 +++++++------- apps/frontend/src/pages/auth/authorize.vue | 42 ++++++++---- .../src/pages/dashboard/affiliate-links.vue | 16 +++-- .../src/pages/dashboard/analytics.vue | 9 ++- .../src/pages/dashboard/collections.vue | 9 ++- apps/frontend/src/pages/dashboard/index.vue | 38 ++++++----- .../src/pages/dashboard/notifications.vue | 26 +++++--- .../src/pages/dashboard/organizations.vue | 14 ++-- .../src/pages/dashboard/revenue/index.vue | 48 ++++++++------ .../src/pages/dashboard/revenue/transfers.vue | 15 +++-- apps/frontend/src/pages/hosting/index.vue | 36 +++++----- .../manage/[id]/options/properties.vue | 66 ++++++++++--------- apps/frontend/src/pages/legal/cmp-info.vue | 13 ++-- .../src/pages/moderation/reports/[id].vue | 28 ++++---- .../src/pages/news/article/[slug].vue | 25 +++---- .../organization/[id]/settings/projects.vue | 14 ++-- .../src/pages/settings/applications.vue | 13 ++-- .../src/pages/settings/authorizations.vue | 49 +++++++------- .../src/pages/settings/billing/charges.vue | 32 ++++----- apps/frontend/src/pages/settings/pats.vue | 7 +- apps/frontend/src/pages/settings/sessions.vue | 8 ++- 27 files changed, 375 insertions(+), 294 deletions(-) diff --git a/apps/frontend/src/components/ui/create/CreateLimitAlert.vue b/apps/frontend/src/components/ui/create/CreateLimitAlert.vue index 35a89692c1..8dbc296769 100644 --- a/apps/frontend/src/components/ui/create/CreateLimitAlert.vue +++ b/apps/frontend/src/components/ui/create/CreateLimitAlert.vue @@ -44,8 +44,11 @@ import { MessageIcon } from '@modrinth/assets' import { Admonition, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui' import { capitalizeString } from '@modrinth/utils' +import { useQuery } from '@tanstack/vue-query' import { computed, watch } from 'vue' +import { useBaseFetch } from '~/composables/fetch.js' + const { formatMessage } = useVIntl() const messages = defineMessages({ @@ -121,10 +124,10 @@ const apiEndpoint = computed(() => { } }) -const { data: limits } = await useAsyncData( - `limits-${props.type}`, - () => useBaseFetch(apiEndpoint.value, { apiVersion: 3 }) as Promise, -) +const { data: limits } = useQuery({ + queryKey: computed(() => ['limits', props.type]), + queryFn: () => useBaseFetch(apiEndpoint.value, { apiVersion: 3 }) as Promise, +}) const typeName = computed<{ singular: string; plural: string }>(() => { switch (props.type) { diff --git a/apps/frontend/src/components/ui/servers/ServerInstallation.vue b/apps/frontend/src/components/ui/servers/ServerInstallation.vue index 88a7db3ffe..f8371865a1 100644 --- a/apps/frontend/src/components/ui/servers/ServerInstallation.vue +++ b/apps/frontend/src/components/ui/servers/ServerInstallation.vue @@ -161,7 +161,10 @@ import { CompassIcon, InfoIcon, SettingsIcon, TransferIcon, UploadIcon } from '@modrinth/assets' import { ButtonStyled, NewProjectCard, useVIntl } from '@modrinth/ui' import type { Loaders } from '@modrinth/utils' +import { useQuery } from '@tanstack/vue-query' +import { computed, ref, watch } from 'vue' +import { useBaseFetch } from '~/composables/fetch.js' import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts' import type { BackupInProgressReason } from '~/pages/hosting/manage/[id].vue' @@ -193,40 +196,40 @@ const data = computed(() => props.server.general) const { data: versions, error: versionsError, - refresh: refreshVersions, -} = await useAsyncData( - `content-loader-versions-${data.value?.upstream?.project_id}`, - async () => { - if (!data.value?.upstream?.project_id) return [] + refetch: refreshVersions, +} = useQuery({ + queryKey: computed(() => ['project', data.value?.upstream?.project_id, 'versions']), + queryFn: async () => { try { - const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`) + const result = await useBaseFetch(`project/${data.value!.upstream!.project_id}/version`) return result || [] } catch (e) { console.error('couldnt fetch all versions:', e) throw new Error('Failed to load modpack versions.') } }, - { default: () => [] }, -) + enabled: computed(() => !!data.value?.upstream?.project_id), + placeholderData: [], +}) const { data: currentVersion, error: currentVersionError, - refresh: refreshCurrentVersion, -} = await useAsyncData( - `content-loader-version-${data.value?.upstream?.version_id}`, - async () => { - if (!data.value?.upstream?.version_id) return null + refetch: refreshCurrentVersion, +} = useQuery({ + queryKey: computed(() => ['version', data.value?.upstream?.version_id]), + queryFn: async () => { try { - const result = await useBaseFetch(`version/${data.value.upstream.version_id}`) + const result = await useBaseFetch(`version/${data.value!.upstream!.version_id}`) return result || null } catch (e) { console.error('couldnt fetch version:', e) throw new Error('Failed to load modpack version.') } }, - { default: () => null }, -) + enabled: computed(() => !!data.value?.upstream?.version_id), + placeholderData: null, +}) const projectCardData = computed(() => ({ icon_url: data.value?.project?.icon_url, diff --git a/apps/frontend/src/layouts/default.vue b/apps/frontend/src/layouts/default.vue index d9451cc468..72f1db6ee8 100644 --- a/apps/frontend/src/layouts/default.vue +++ b/apps/frontend/src/layouts/default.vue @@ -720,6 +720,7 @@ import { useVIntl, } from '@modrinth/ui' import { isAdmin, isStaff, UserBadge } from '@modrinth/utils' +import { useQuery } from '@tanstack/vue-query' import TextLogo from '~/components/brand/TextLogo.vue' import BatchCreditModal from '~/components/ui/admin/BatchCreditModal.vue' @@ -754,9 +755,10 @@ const route = useNativeRoute() const router = useNativeRouter() const link = config.public.siteUrl + route.path.replace(/\/+$/, '') -const { data: payoutBalance } = await useAsyncData('payout/balance', () => { - if (!auth.value.user) return null - return useBaseFetch('payout/balance', { apiVersion: 3 }) +const { data: payoutBalance } = useQuery({ + queryKey: ['payout', 'balance'], + queryFn: () => useBaseFetch('payout/balance', { apiVersion: 3 }), + enabled: computed(() => !!auth.value.user), }) const showTaxComplianceBanner = computed(() => { diff --git a/apps/frontend/src/pages/[type]/[id]/moderation.vue b/apps/frontend/src/pages/[type]/[id]/moderation.vue index 276b911891..d3b0c621b4 100644 --- a/apps/frontend/src/pages/[type]/[id]/moderation.vue +++ b/apps/frontend/src/pages/[type]/[id]/moderation.vue @@ -101,8 +101,11 @@ diff --git a/apps/frontend/src/pages/dashboard/collections.vue b/apps/frontend/src/pages/dashboard/collections.vue index 45ac4b75c9..4a327b1fc8 100644 --- a/apps/frontend/src/pages/dashboard/collections.vue +++ b/apps/frontend/src/pages/dashboard/collections.vue @@ -107,8 +107,10 @@ import { XIcon, } from '@modrinth/assets' import { Avatar, Button, commonMessages, defineMessages, 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 = useCompactNumber() @@ -153,9 +155,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 orderedCollections = computed(() => { if (!collections.value) return [] diff --git a/apps/frontend/src/pages/dashboard/index.vue b/apps/frontend/src/pages/dashboard/index.vue index c170714d28..68b3d2714c 100644 --- a/apps/frontend/src/pages/dashboard/index.vue +++ b/apps/frontend/src/pages/dashboard/index.vue @@ -35,7 +35,7 @@ :auth="auth" raised compact - @update:notifications="() => refresh()" + @update:notifications="() => refetch()" /> 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)) diff --git a/apps/frontend/src/pages/dashboard/notifications.vue b/apps/frontend/src/pages/dashboard/notifications.vue index 223b12fe53..f526723004 100644 --- a/apps/frontend/src/pages/dashboard/notifications.vue +++ b/apps/frontend/src/pages/dashboard/notifications.vue @@ -29,7 +29,7 @@ :format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')" :capitalize="false" /> -

Loading notifications...

+

Loading notifications...

You don't have any unread notifications.

@@ -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) { diff --git a/apps/frontend/src/pages/dashboard/organizations.vue b/apps/frontend/src/pages/dashboard/organizations.vue index c701b35455..d610dbee11 100644 --- a/apps/frontend/src/pages/dashboard/organizations.vue +++ b/apps/frontend/src/pages/dashboard/organizations.vue @@ -51,6 +51,7 @@ diff --git a/apps/frontend/src/pages/news/article/[slug].vue b/apps/frontend/src/pages/news/article/[slug].vue index d7d78dcb63..ddb1e38c5f 100644 --- a/apps/frontend/src/pages/news/article/[slug].vue +++ b/apps/frontend/src/pages/news/article/[slug].vue @@ -3,6 +3,7 @@ import { GitGraphIcon, RssIcon } from '@modrinth/assets' import { articles as rawArticles } from '@modrinth/blog' import { Avatar, ButtonStyled } from '@modrinth/ui' import type { User } from '@modrinth/utils' +import { useQuery } from '@tanstack/vue-query' import dayjs from 'dayjs' import { computed, onMounted } from 'vue' @@ -24,19 +25,19 @@ if (!rawArticle) { const authorsUrl = `users?ids=${JSON.stringify(rawArticle.authors)}` -const [authors, html] = await Promise.all([ - rawArticle.authors - ? useAsyncData(authorsUrl, () => useBaseFetch(authorsUrl)).then((data) => { - const users = data.data as Ref - users.value.sort((a, b) => { - return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id) - }) +const { data: authors } = useQuery({ + queryKey: computed(() => ['users', rawArticle.authors]), + queryFn: async () => { + const users = (await useBaseFetch(authorsUrl)) as User[] + users.sort((a, b) => { + return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id) + }) + return users + }, + enabled: computed(() => !!rawArticle.authors), +}) - return users - }) - : Promise.resolve(), - rawArticle.html(), -]) +const html = await rawArticle.html() const article = computed(() => ({ ...rawArticle, diff --git a/apps/frontend/src/pages/organization/[id]/settings/projects.vue b/apps/frontend/src/pages/organization/[id]/settings/projects.vue index 4a31e9f7b6..0a1eadd58d 100644 --- a/apps/frontend/src/pages/organization/[id]/settings/projects.vue +++ b/apps/frontend/src/pages/organization/[id]/settings/projects.vue @@ -337,6 +337,7 @@ import { useVIntl, } from '@modrinth/ui' import { formatProjectType } from '@modrinth/utils' +import { useQuery } from '@tanstack/vue-query' import { Multiselect } from 'vue-multiselect' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' @@ -351,13 +352,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([]) diff --git a/apps/frontend/src/pages/settings/applications.vue b/apps/frontend/src/pages/settings/applications.vue index e62624b233..8c8375b179 100644 --- a/apps/frontend/src/pages/settings/applications.vue +++ b/apps/frontend/src/pages/settings/applications.vue @@ -228,6 +228,7 @@ import { injectNotificationManager, useVIntl, } from '@modrinth/ui' +import { useQuery } from '@tanstack/vue-query' import Modal from '~/components/ui/Modal.vue' import { @@ -269,16 +270,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) { diff --git a/apps/frontend/src/pages/settings/authorizations.vue b/apps/frontend/src/pages/settings/authorizations.vue index 74fa5c556c..7ef5ab0838 100644 --- a/apps/frontend/src/pages/settings/authorizations.vue +++ b/apps/frontend/src/pages/settings/authorizations.vue @@ -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) { diff --git a/apps/frontend/src/pages/settings/billing/charges.vue b/apps/frontend/src/pages/settings/billing/charges.vue index b4c55f419d..8a5b6fccbb 100644 --- a/apps/frontend/src/pages/settings/billing/charges.vue +++ b/apps/frontend/src/pages/settings/billing/charges.vue @@ -40,6 +40,7 @@ diff --git a/apps/frontend/src/pages/settings/pats.vue b/apps/frontend/src/pages/settings/pats.vue index 8294f2ae59..27e8d64f00 100644 --- a/apps/frontend/src/pages/settings/pats.vue +++ b/apps/frontend/src/pages/settings/pats.vue @@ -215,6 +215,7 @@ import { useRelativeTime, useVIntl, } from '@modrinth/ui' +import { useQuery } from '@tanstack/vue-query' import Modal from '~/components/ui/Modal.vue' import { @@ -333,7 +334,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)) }) diff --git a/apps/frontend/src/pages/settings/sessions.vue b/apps/frontend/src/pages/settings/sessions.vue index 5cef936725..5c7cd71187 100644 --- a/apps/frontend/src/pages/settings/sessions.vue +++ b/apps/frontend/src/pages/settings/sessions.vue @@ -65,6 +65,7 @@ import { useRelativeTime, useVIntl, } from '@modrinth/ui' +import { useQuery } from '@tanstack/vue-query' definePageMeta({ middleware: 'auth', @@ -110,9 +111,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() From 98d95b611e6d4fe26e6151e214a4310af706c40e Mon Sep 17 00:00:00 2001 From: tdgao Date: Mon, 2 Feb 2026 11:23:34 -0700 Subject: [PATCH 2/9] refactor report list and report view --- .../src/components/ui/report/ReportView.vue | 130 +++++++++------- .../src/components/ui/report/ReportsList.vue | 144 +++++++++++------- 2 files changed, 165 insertions(+), 109 deletions(-) diff --git a/apps/frontend/src/components/ui/report/ReportView.vue b/apps/frontend/src/components/ui/report/ReportView.vue index 67594cbb92..57227bf586 100644 --- a/apps/frontend/src/components/ui/report/ReportView.vue +++ b/apps/frontend/src/components/ui/report/ReportView.vue @@ -21,9 +21,13 @@