feat: switch world route impl

This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:22 +01:00
parent 8381482cbb
commit 0a451d9f9a
4 changed files with 142 additions and 12 deletions
@@ -177,22 +177,41 @@
{{ formatMessage(messages.editInstance) }}
</button>
</ButtonStyled>
<ButtonStyled circular>
<button
v-tooltip="formatMessage(messages.instanceSettings)"
class="!shadow-none"
@click="emit('settings', world.id)"
>
<Settings2Icon aria-hidden="true" />
</button>
</ButtonStyled>
<div class="flex items-center gap-2">
<ButtonStyled v-if="!world.active" color="brand">
<button
class="!shadow-none"
:disabled="switching"
@click="emit('switch', world.id)"
>
<LoaderCircleIcon v-if="switching" class="animate-spin" aria-hidden="true" />
<RefreshCwIcon v-else aria-hidden="true" />
{{ formatMessage(messages.switchInstance) }}
</button>
</ButtonStyled>
<ButtonStyled circular>
<button
v-tooltip="formatMessage(messages.instanceSettings)"
class="!shadow-none"
@click="emit('settings', world.id)"
>
<Settings2Icon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</footer>
</template>
</article>
</template>
<script setup lang="ts">
import { PencilIcon, PlusIcon, Settings2Icon } from '@modrinth/assets'
import {
LoaderCircleIcon,
PencilIcon,
PlusIcon,
RefreshCwIcon,
Settings2Icon,
} from '@modrinth/assets'
import { capitalizeString } from '@modrinth/utils'
import { computed, useId, useTemplateRef } from 'vue'
@@ -238,6 +257,10 @@ const messages = defineMessages({
id: 'servers.manage.instances.card.edit',
defaultMessage: 'Edit instance',
},
switchInstance: {
id: 'servers.manage.instances.card.switch',
defaultMessage: 'Switch to instance',
},
instanceSettings: {
id: 'servers.manage.instances.card.settings',
defaultMessage: 'Instance settings',
@@ -275,11 +298,13 @@ type EmptyWorld = {
const props = defineProps<{
world: UsedWorld | EmptyWorld
switching?: boolean
}>()
const emit = defineEmits<{
create: [slotId: string]
edit: [worldId: string]
switch: [worldId: string]
settings: [worldId: string]
}>()
@@ -927,6 +927,9 @@ async function confirmResetToOnboarding() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] }),
queryClient.invalidateQueries({
queryKey: ['servers', 'worlds', 'summary', 'v1', serverId],
}),
])
addNotification({
type: 'success',
@@ -40,8 +40,10 @@
v-for="world in worldSlots"
:key="world.id"
:world="world"
:switching="world.type === 'world' && switchingWorldId === world.id"
@create="handleCreateWorld"
@edit="handleEditWorld"
@switch="handleSwitchWorld"
@settings="handleWorldSettings"
/>
</div>
@@ -51,9 +53,9 @@
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { InfoIcon, XIcon } from '@modrinth/assets'
import { useQuery } from '@tanstack/vue-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
@@ -62,6 +64,7 @@ import { defineMessages, useVIntl } from '#ui/composables/i18n'
import {
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
injectServerSettingsModal,
} from '#ui/providers'
import { formatLoaderLabel } from '#ui/utils/loaders'
@@ -84,6 +87,18 @@ const messages = defineMessages({
id: 'servers.manage.instances.info.dismiss',
defaultMessage: "Don't show this again",
},
switchSuccessTitle: {
id: 'servers.manage.instances.switch.success.title',
defaultMessage: 'Instance switched',
},
switchSuccessText: {
id: 'servers.manage.instances.switch.success.text',
defaultMessage: '{instance} is now the active instance.',
},
switchError: {
id: 'servers.manage.instances.switch.error',
defaultMessage: 'Failed to switch instance',
},
})
type LinkedModpack = {
@@ -125,9 +140,12 @@ const INSTANCE_INFO_ADMONITION_KEY = 'server-instances-info-admonition-dismissed
const client = injectModrinthClient()
const { serverId, server, isServerRunning } = injectModrinthServerContext()
const { openServerInstanceSettings } = injectServerSettingsModal()
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
const router = useRouter()
const queryClient = useQueryClient()
const instanceInfoAdmonitionDismissed = useStorage(INSTANCE_INFO_ADMONITION_KEY, false)
const switchingWorldId = ref<string | null>(null)
const worldsQuery = useQuery({
queryKey: computed(() => ['servers', 'worlds', 'summary', 'v1', serverId]),
@@ -137,6 +155,38 @@ const worldsQuery = useQuery({
const worldsPending = computed(() => worldsQuery.isLoading.value && !worldsQuery.data.value)
const worldSlots = computed(() => worldsQuery.data.value ?? [])
const switchWorldMutation = useMutation({
mutationFn: (worldId: string) => client.archon.servers_v1.switchWorld(serverId, worldId),
onMutate: (worldId) => {
switchingWorldId.value = worldId
},
onSuccess: async (_, worldId) => {
updateActiveWorld(worldId)
await Promise.all([
queryClient.invalidateQueries({
queryKey: ['servers', 'worlds', 'summary', 'v1', serverId],
}),
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] }),
])
updateActiveWorld(worldId)
const worldName = getWorldSlotName(worldId)
addNotification({
type: 'success',
title: formatMessage(messages.switchSuccessTitle),
text: formatMessage(messages.switchSuccessText, { instance: worldName }),
})
},
onError: (error) => {
addNotification({
type: 'error',
text: error instanceof Error ? error.message : formatMessage(messages.switchError),
})
},
onSettled: () => {
switchingWorldId.value = null
},
})
async function loadWorldSlots(): Promise<WorldSlot[]> {
try {
@@ -322,6 +372,12 @@ function handleEditWorld(worldId: string) {
)
}
function handleSwitchWorld(worldId: string) {
const world = worldSlots.value.find((slot) => slot.type === 'world' && slot.id === worldId)
if (world?.type !== 'world' || world.active || switchWorldMutation.isPending.value) return
switchWorldMutation.mutate(worldId)
}
function handleWorldSettings(worldId: string) {
openServerInstanceSettings({ tabId: 'general', worldId })
}
@@ -333,4 +389,38 @@ function handleCreateWorld() {
function dismissInstanceInfoAdmonition() {
instanceInfoAdmonitionDismissed.value = true
}
function updateActiveWorld(worldId: string) {
queryClient.setQueryData<WorldSlot[]>(
['servers', 'worlds', 'summary', 'v1', serverId],
(current) =>
current?.map((slot) =>
slot.type === 'world'
? {
...slot,
active: slot.id === worldId,
lastActiveAt: slot.id === worldId ? new Date().toISOString() : slot.lastActiveAt,
}
: slot,
),
)
queryClient.setQueryData<Archon.Servers.v1.ServerFull>(
['servers', 'v1', 'detail', serverId],
(current) =>
current
? {
...current,
worlds: current.worlds.map((world) => ({
...world,
is_active: world.id === worldId,
})),
}
: current,
)
}
function getWorldSlotName(worldId: string) {
const world = worldSlots.value.find((slot) => slot.type === 'world' && slot.id === worldId)
return world?.name ?? formatMessage(messages.instanceSlotName, { index: 1 })
}
</script>