From 8d6179f71fca52c4622b1ff540e1752a55fb9182 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 27 Apr 2026 14:10:10 +0100 Subject: [PATCH 1/3] feat: reshuffle layout for worlds --- apps/app-frontend/src/App.vue | 49 ++++ .../src/pages/hosting/manage/Backups.vue | 2 +- .../src/pages/hosting/manage/Content.vue | 2 +- .../src/pages/hosting/manage/Index.vue | 2 +- .../src/pages/hosting/manage/World.vue | 16 ++ .../src/pages/hosting/manage/index.js | 3 +- .../providers/setup/server-install-content.ts | 9 +- apps/app-frontend/src/routes.js | 34 +++ .../middleware/server-world-tabs.global.ts | 40 ++++ .../src/pages/discover/[type]/index.vue | 16 +- .../hosting/manage/[id]/worlds/[world_id].vue | 11 + .../[id]/{ => worlds/[world_id]}/backups.vue | 2 +- .../[id]/{ => worlds/[world_id]}/files.vue | 0 .../[world_id]/index.vue} | 2 +- .../servers/ServerSettingsModal.vue | 2 +- .../admonitions/ServerPanelAdmonitions.vue | 6 +- .../servers/backups/BackupCreateModal.vue | 4 +- .../servers/backups/BackupRenameModal.vue | 4 +- .../servers/backups/BackupRestoreModal.vue | 6 +- .../server-header/PanelServerActionButton.vue | 21 +- .../server-header/ServerManageHeader.vue | 144 +++++------- .../src/composables/server-backups-queue.ts | 2 +- .../server-settings/pages/installation.vue | 11 +- .../server-settings/pages/properties.vue | 6 +- .../hosting/manage/[id]/onboarding.vue | 6 +- .../wrapped/hosting/manage/backups.vue | 6 +- .../manage/components/ServerManageStats.vue | 6 +- .../wrapped/hosting/manage/content.vue | 8 +- .../layouts/wrapped/hosting/manage/root.vue | 81 ++++--- .../wrapped/hosting/manage/world-root.vue | 213 ++++++++++++++++++ .../layouts/wrapped/hosting/manage/worlds.vue | 12 +- packages/ui/src/layouts/wrapped/index.ts | 1 + .../servers/ServerPanelAdmonitions.stories.ts | 2 +- 33 files changed, 550 insertions(+), 179 deletions(-) create mode 100644 apps/app-frontend/src/pages/hosting/manage/World.vue create mode 100644 apps/frontend/src/middleware/server-world-tabs.global.ts create mode 100644 apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id].vue rename apps/frontend/src/pages/hosting/manage/[id]/{ => worlds/[world_id]}/backups.vue (93%) rename apps/frontend/src/pages/hosting/manage/[id]/{ => worlds/[world_id]}/files.vue (100%) rename apps/frontend/src/pages/hosting/manage/[id]/{content.vue => worlds/[world_id]/index.vue} (92%) create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/world-root.vue diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 5cb3b66d26..c51fc87cc2 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -439,6 +439,11 @@ const sidebarOverlayScrollbarsOptions = Object.freeze({ }, }) +router.beforeEach(async (to) => { + const redirect = await resolveLegacyServerWorldTabRedirect(to) + if (redirect) return redirect +}) + router.beforeEach(() => { suspensePending = false if (routerToken) loading.end(routerToken) @@ -470,6 +475,50 @@ function onSuspensePending() { suspenseToken = loading.begin() } +async function resolveLegacyServerWorldTabRedirect(to) { + if (!['ServerManageContent', 'ServerManageFiles', 'ServerManageBackups'].includes(to.name)) { + return null + } + + const serverId = getRouteParam(to.params.id) + if (!serverId) return null + + const tabPath = + to.name === 'ServerManageFiles' ? '/files' : to.name === 'ServerManageBackups' ? '/backups' : '' + const worldsPath = `/hosting/manage/${encodeURIComponent(serverId)}/worlds` + + try { + const serverFull = await tauriApiClient.archon.servers_v1.get(serverId) + const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0] + if (world) { + return { + path: `${worldsPath}/${encodeURIComponent(world.id)}${tabPath}`, + query: to.query, + hash: to.hash, + replace: true, + } + } + } catch { + return { + path: worldsPath, + query: to.query, + hash: to.hash, + replace: true, + } + } + + return { + path: worldsPath, + query: to.query, + hash: to.hash, + replace: true, + } +} + +function getRouteParam(param) { + return Array.isArray(param) ? param[0] : param +} + function onSuspenseResolve() { if (suspenseToken) { loading.end(suspenseToken) diff --git a/apps/app-frontend/src/pages/hosting/manage/Backups.vue b/apps/app-frontend/src/pages/hosting/manage/Backups.vue index 0ebb7bd800..b62de2ba6d 100644 --- a/apps/app-frontend/src/pages/hosting/manage/Backups.vue +++ b/apps/app-frontend/src/pages/hosting/manage/Backups.vue @@ -13,7 +13,7 @@ const queryClient = useQueryClient() if (worldId.value) { try { await queryClient.ensureQueryData({ - queryKey: ['backups', 'list', serverId], + queryKey: ['backups', 'list', serverId, worldId.value], queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!), staleTime: 30_000, }) diff --git a/apps/app-frontend/src/pages/hosting/manage/Content.vue b/apps/app-frontend/src/pages/hosting/manage/Content.vue index ab7d242452..1306234cc3 100644 --- a/apps/app-frontend/src/pages/hosting/manage/Content.vue +++ b/apps/app-frontend/src/pages/hosting/manage/Content.vue @@ -13,7 +13,7 @@ const queryClient = useQueryClient() if (worldId.value) { try { await queryClient.ensureQueryData({ - queryKey: ['content', 'list', 'v1', serverId], + queryKey: ['content', 'list', 'v1', serverId, worldId.value], queryFn: () => client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }), staleTime: 30_000, diff --git a/apps/app-frontend/src/pages/hosting/manage/Index.vue b/apps/app-frontend/src/pages/hosting/manage/Index.vue index f1e4388755..030f74a7e3 100644 --- a/apps/app-frontend/src/pages/hosting/manage/Index.vue +++ b/apps/app-frontend/src/pages/hosting/manage/Index.vue @@ -93,7 +93,7 @@ watch( breadcrumbs.setName('Server', server.name) breadcrumbs.setContext({ name: server.name, - link: `/hosting/manage/${serverId.value}/content`, + link: `/hosting/manage/${serverId.value}/worlds`, }) } }, diff --git a/apps/app-frontend/src/pages/hosting/manage/World.vue b/apps/app-frontend/src/pages/hosting/manage/World.vue new file mode 100644 index 0000000000..fe52492c50 --- /dev/null +++ b/apps/app-frontend/src/pages/hosting/manage/World.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/app-frontend/src/pages/hosting/manage/index.js b/apps/app-frontend/src/pages/hosting/manage/index.js index e06f46e2a2..6e77f9e638 100644 --- a/apps/app-frontend/src/pages/hosting/manage/index.js +++ b/apps/app-frontend/src/pages/hosting/manage/index.js @@ -3,6 +3,7 @@ import Content from './Content.vue' import Files from './Files.vue' import Index from './Index.vue' import Overview from './Overview.vue' +import World from './World.vue' import Worlds from './Worlds.vue' -export { Backups, Content, Files, Index, Overview, Worlds } +export { Backups, Content, Files, Index, Overview, World, Worlds } diff --git a/apps/app-frontend/src/providers/setup/server-install-content.ts b/apps/app-frontend/src/providers/setup/server-install-content.ts index 35919d9d73..021d23b5d9 100644 --- a/apps/app-frontend/src/providers/setup/server-install-content.ts +++ b/apps/app-frontend/src/providers/setup/server-install-content.ts @@ -101,7 +101,7 @@ export function createServerInstallContent(opts: { if (serverFlowFrom.value === 'reset-server') { return `/hosting/manage/${sid}?openSettings=installation` } - return `/hosting/manage/${sid}/content` + return getServerWorldContentPath(sid, effectiveServerWorldId.value) }) const serverBackLabel = computed(() => { if (serverFlowFrom.value === 'onboarding') return 'Back to setup' @@ -351,7 +351,7 @@ export function createServerInstallContent(opts: { if (serverFlowFrom.value === 'onboarding') { await client.archon.servers_v1.endIntro(sid) - await router.push(`/hosting/manage/${sid}/content`) + await router.push(getServerWorldContentPath(sid, wid)) return } @@ -366,6 +366,11 @@ export function createServerInstallContent(opts: { serverContentProjectIds.value = new Set([...serverContentProjectIds.value, id]) } + function getServerWorldContentPath(serverId: string, worldId: string | null) { + const base = `/hosting/manage/${encodeURIComponent(serverId)}/worlds` + return worldId ? `${base}/${encodeURIComponent(worldId)}` : base + } + return { serverIdQuery, worldIdQuery, diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index 410869c46a..e0b131f3d7 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -65,6 +65,40 @@ export default new createRouter({ breadcrumb: [{ name: '?Server' }], }, }, + { + path: 'worlds/:world_id', + name: 'ServerManageWorld', + component: Hosting.World, + meta: { + breadcrumb: [{ name: '?Server' }], + }, + children: [ + { + path: '', + name: 'ServerManageWorldContent', + component: Hosting.Content, + meta: { + breadcrumb: [{ name: '?Server' }], + }, + }, + { + path: 'files', + name: 'ServerManageWorldFiles', + component: Hosting.Files, + meta: { + breadcrumb: [{ name: '?Server' }], + }, + }, + { + path: 'backups', + name: 'ServerManageWorldBackups', + component: Hosting.Backups, + meta: { + breadcrumb: [{ name: '?Server' }], + }, + }, + ], + }, { path: 'files', name: 'ServerManageFiles', diff --git a/apps/frontend/src/middleware/server-world-tabs.global.ts b/apps/frontend/src/middleware/server-world-tabs.global.ts new file mode 100644 index 0000000000..2a740b7dea --- /dev/null +++ b/apps/frontend/src/middleware/server-world-tabs.global.ts @@ -0,0 +1,40 @@ +import { createModrinthClient } from '~/helpers/api.ts' + +export default defineNuxtRouteMiddleware(async (to) => { + const match = to.path.match(/^\/hosting\/manage\/([^/]+)\/(content|files|backups)\/?$/) + if (!match) return + + const serverId = decodeURIComponent(match[1]) + const tab = match[2] + const worldsPath = `/hosting/manage/${encodeURIComponent(serverId)}/worlds` + const tabPath = tab === 'content' ? '' : `/${tab}` + const auth = await useAuth() + + if (auth.value.token) { + try { + const config = useRuntimeConfig() + const client = createModrinthClient(auth, { + apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'), + archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'), + rateLimitKey: config.rateLimitKey, + }) + const serverFull = await client.archon.servers_v1.get(serverId) + const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0] + + if (world) { + return navigateTo( + { + path: `${worldsPath}/${encodeURIComponent(world.id)}${tabPath}`, + query: to.query, + hash: to.hash, + }, + { replace: true }, + ) + } + } catch { + return navigateTo({ path: worldsPath, query: to.query, hash: to.hash }, { replace: true }) + } + } + + return navigateTo({ path: worldsPath, query: to.query, hash: to.hash }, { replace: true }) +}) diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue index 96985c19bd..8f041e1f3f 100644 --- a/apps/frontend/src/pages/discover/[type]/index.vue +++ b/apps/frontend/src/pages/discover/[type]/index.vue @@ -211,7 +211,10 @@ function syncHiddenInstalledProjectIds() { hiddenInstalledProjectIdsInitialized.value = true } -const contentQueryKey = computed(() => ['content', 'list', currentServerId.value ?? ''] as const) +const contentQueryKey = computed( + () => + ['content', 'list', 'v1', currentServerId.value ?? '', currentWorldId.value ?? null] as const, +) const { data: serverContentData, error: serverContentError } = useQuery({ queryKey: contentQueryKey, queryFn: () => client.archon.content_v1.getAddons(currentServerId.value!, currentWorldId.value!), @@ -252,7 +255,7 @@ const installContentMutation = useMutation({ }), onSuccess: () => { if (currentServerId.value) { - queryClient.refetchQueries({ queryKey: ['content', 'list', currentServerId.value] }) + queryClient.refetchQueries({ queryKey: contentQueryKey.value }) } }, }) @@ -551,7 +554,7 @@ async function onModpackFlowCreate(config: CreationFlowContextValue) { if (fromContext.value === 'onboarding') { await client.archon.servers_v1.endIntro(currentServerId.value) queryClient.invalidateQueries({ queryKey: ['servers', 'detail', currentServerId.value] }) - navigateTo(`/hosting/manage/${currentServerId.value}/content`) + navigateTo(getServerWorldContentPath(currentServerId.value, currentWorldId.value ?? null)) } else { navigateTo(`/hosting/manage/${currentServerId.value}?openSettings=installation`) } @@ -566,9 +569,14 @@ const serverBackUrl = computed(() => { const id = serverData.value.server_id if (fromContext.value === 'onboarding') return `/hosting/manage/${id}?resumeModal=setup-type` if (fromContext.value === 'reset-server') return `/hosting/manage/${id}?openSettings=installation` - return `/hosting/manage/${id}/content` + return getServerWorldContentPath(id, currentWorldId.value ?? null) }) +function getServerWorldContentPath(serverId: string, worldId: string | null) { + const base = `/hosting/manage/${encodeURIComponent(serverId)}/worlds` + return worldId ? `${base}/${encodeURIComponent(worldId)}` : base +} + const serverBackLabel = computed(() => { if (fromContext.value === 'onboarding') return 'Back to setup' if (fromContext.value === 'reset-server') return 'Cancel reset' diff --git a/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id].vue b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id].vue new file mode 100644 index 0000000000..357dc14f4a --- /dev/null +++ b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id].vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/frontend/src/pages/hosting/manage/[id]/backups.vue b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/backups.vue similarity index 93% rename from apps/frontend/src/pages/hosting/manage/[id]/backups.vue rename to apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/backups.vue index 467662653f..ab36852f6a 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/backups.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/backups.vue @@ -14,7 +14,7 @@ const flags = useFeatureFlags() if (worldId.value) { try { await queryClient.ensureQueryData({ - queryKey: ['backups', 'list', serverId], + queryKey: ['backups', 'list', serverId, worldId.value], queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!), staleTime: 30_000, }) diff --git a/apps/frontend/src/pages/hosting/manage/[id]/files.vue b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/files.vue similarity index 100% rename from apps/frontend/src/pages/hosting/manage/[id]/files.vue rename to apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/files.vue diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content.vue b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/index.vue similarity index 92% rename from apps/frontend/src/pages/hosting/manage/[id]/content.vue rename to apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/index.vue index 6e130515b0..a5cd8e6ae6 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/content.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/worlds/[world_id]/index.vue @@ -13,7 +13,7 @@ const queryClient = useQueryClient() if (worldId.value) { try { await queryClient.ensureQueryData({ - queryKey: ['content', 'list', 'v1', serverId], + queryKey: ['content', 'list', 'v1', serverId, worldId.value], queryFn: () => client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }), staleTime: 30_000, diff --git a/packages/ui/src/components/servers/ServerSettingsModal.vue b/packages/ui/src/components/servers/ServerSettingsModal.vue index a72001c73b..6575ad2afa 100644 --- a/packages/ui/src/components/servers/ServerSettingsModal.vue +++ b/packages/ui/src/components/servers/ServerSettingsModal.vue @@ -186,7 +186,7 @@ async function show({ serverId, tabIndex, tabId }: ShowOptions) { queryFn: () => client.archon.properties_v1.getProperties(targetServerId, worldId.value!), }) queryClient.prefetchQuery({ - queryKey: ['content', 'list', 'v1', targetServerId], + queryKey: ['content', 'list', 'v1', targetServerId, worldId.value], queryFn: () => client.archon.content_v1.getAddons(targetServerId, worldId.value!, { from_modpack: false, diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue index e976f60685..8e0f4dcfa6 100644 --- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue +++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue @@ -55,8 +55,12 @@ const messages = defineMessages({ }, }) -const isOnContentTab = computed(() => route.path.includes('/content')) const isOnFilesTab = computed(() => route.path.includes('/files')) +const isOnContentTab = computed( + () => + route.path.includes('/content') || + (!!route.params.world_id && !isOnFilesTab.value && !route.path.includes('/backups')), +) const bannerCoversInstalling = computed( () => ctx.server.value?.status === 'installing' || ctx.isSyncingContent.value, diff --git a/packages/ui/src/components/servers/backups/BackupCreateModal.vue b/packages/ui/src/components/servers/backups/BackupCreateModal.vue index ded244ba2e..e66d837218 100644 --- a/packages/ui/src/components/servers/backups/BackupCreateModal.vue +++ b/packages/ui/src/components/servers/backups/BackupCreateModal.vue @@ -87,12 +87,12 @@ const props = defineProps<{ backups?: Archon.BackupsQueue.v1.BackupQueueBackup[] }>() -const backupsQueryKey = ['backups', 'queue', ctx.serverId] +const backupsQueryKey = computed(() => ['backups', 'queue', ctx.serverId, ctx.worldId.value]) const createMutation = useMutation({ mutationFn: (name: string) => client.archon.backups_queue_v1.create(ctx.serverId, ctx.worldId.value!, { name }), - onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey.value }), }) const modal = ref>() diff --git a/packages/ui/src/components/servers/backups/BackupRenameModal.vue b/packages/ui/src/components/servers/backups/BackupRenameModal.vue index b06ffc7c08..0c78de0fbc 100644 --- a/packages/ui/src/components/servers/backups/BackupRenameModal.vue +++ b/packages/ui/src/components/servers/backups/BackupRenameModal.vue @@ -69,12 +69,12 @@ const props = defineProps<{ backups?: Archon.BackupsQueue.v1.BackupQueueBackup[] }>() -const backupsQueryKey = ['backups', 'queue', ctx.serverId] +const backupsQueryKey = computed(() => ['backups', 'queue', ctx.serverId, ctx.worldId.value]) const renameMutation = useMutation({ mutationFn: ({ backupId, name }: { backupId: string; name: string }) => client.archon.backups_v1.rename(ctx.serverId, ctx.worldId.value!, backupId, { name }), - onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey.value }), }) const modal = ref>() diff --git a/packages/ui/src/components/servers/backups/BackupRestoreModal.vue b/packages/ui/src/components/servers/backups/BackupRestoreModal.vue index 2892bbe066..9fb4e48a49 100644 --- a/packages/ui/src/components/servers/backups/BackupRestoreModal.vue +++ b/packages/ui/src/components/servers/backups/BackupRestoreModal.vue @@ -39,7 +39,7 @@ import type { Archon } from '@modrinth/api-client' import { RotateCounterClockwiseIcon, SpinnerIcon, XIcon } from '@modrinth/assets' import { useMutation, useQueryClient } from '@tanstack/vue-query' -import { ref } from 'vue' +import { computed, ref } from 'vue' import { injectModrinthClient, @@ -56,7 +56,7 @@ const client = injectModrinthClient() const queryClient = useQueryClient() const ctx = injectModrinthServerContext() -const backupsQueryKey = ['backups', 'queue', ctx.serverId] +const backupsQueryKey = computed(() => ['backups', 'queue', ctx.serverId, ctx.worldId.value]) function safetyBackupName(backupName: string) { const base = `Before restoring "${backupName}"` @@ -66,7 +66,7 @@ function safetyBackupName(backupName: string) { const restoreMutation = useMutation({ mutationFn: ({ backupId, name }: { backupId: string; name: string }) => client.archon.backups_queue_v1.restore(ctx.serverId, ctx.worldId.value!, backupId, { name }), - onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey.value }), }) const modal = ref>() diff --git a/packages/ui/src/components/servers/server-header/PanelServerActionButton.vue b/packages/ui/src/components/servers/server-header/PanelServerActionButton.vue index fd6cd6930d..256f98bf37 100644 --- a/packages/ui/src/components/servers/server-header/PanelServerActionButton.vue +++ b/packages/ui/src/components/servers/server-header/PanelServerActionButton.vue @@ -1,14 +1,14 @@