feat: server settings split up + copy changes across panel warning modals

This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:22 +01:00
parent 27f629a7a3
commit 9ab9fe994f
41 changed files with 1030 additions and 530 deletions
@@ -101,10 +101,7 @@ export class ArchonServersV0Module extends AbstractModule {
* Send a power action to a server (Start, Stop, Restart, Kill)
* POST /modrinth/v0/servers/:id/power
*/
public async power(
serverId: string,
action: Archon.Servers.v0.PowerAction,
): Promise<void> {
public async power(serverId: string, action: Archon.Servers.v0.PowerAction): Promise<void> {
await this.client.request(`/servers/${serverId}/power`, {
api: 'archon',
method: 'POST',
@@ -285,7 +285,7 @@ const messages = defineMessages({
},
beforeResetServerBackupName: {
id: 'creation-flow.modal.final-config.backup.before-reset-server.name',
defaultMessage: 'Before reset server',
defaultMessage: 'Before reset instance',
},
})
@@ -46,7 +46,7 @@ export const creationFlowMessages = defineMessages({
},
resetServerTitle: {
id: 'creation-flow.title.reset-server',
defaultMessage: 'Reset server',
defaultMessage: 'Reset instance',
},
createInstanceTitle: {
id: 'creation-flow.title.create-instance',
@@ -42,7 +42,7 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
const label = isWorld
? ctx.formatMessage(creationFlowMessages.createWorldButton)
: isReset
? ctx.formatMessage(commonMessages.resetServerButton)
? ctx.formatMessage(creationFlowMessages.resetServerTitle)
: isOnboarding
? ctx.formatMessage(creationFlowMessages.setupServerButton)
: ctx.formatMessage(commonMessages.continueButton)
@@ -0,0 +1,246 @@
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { ChevronRightIcon } from '@modrinth/assets'
import { useQueryClient } from '@tanstack/vue-query'
import { computed, nextTick, ref } from 'vue'
import type { TabbedModalTab } from '#ui/components'
import { TabbedModal } from '#ui/components'
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n'
import {
ServerInstanceSettingsAdvancedPage,
ServerInstanceSettingsGeneralPage,
serverInstanceSettingsTabDefinitions,
type ServerInstanceSettingsTabId,
ServerSettingsInstallationPage,
ServerSettingsPropertiesPage,
} from '#ui/layouts/shared/server-settings'
import { provideServerSettings } from '#ui/layouts/shared/server-settings/providers/server-settings'
import {
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
provideModrinthServerContext,
} from '#ui/providers'
import { commonMessages } from '#ui/utils/common-messages'
type ShowOptions = {
serverId: string
tabIndex?: number
tabId?: ServerInstanceSettingsTabId
worldId?: string | null
}
const props = defineProps<{
resolveViewer: () => Promise<{ userId: string | null; userRole: string | null }>
browseModpacks?: (args: {
serverId: string
worldId: string | null
from: 'reset-server'
}) => void | Promise<void>
}>()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const messages = defineMessages({
failedToLoadServer: {
id: 'app.server-instance-settings.failed-to-load-server',
defaultMessage: 'Failed to load instance settings',
},
})
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
const baseServerContext = injectModrinthServerContext()
const selectedWorldId = ref<string | null>(baseServerContext.worldId.value)
const modalServerContext = {
...baseServerContext,
worldId: selectedWorldId,
} satisfies ReturnType<typeof injectModrinthServerContext>
provideModrinthServerContext(modalServerContext)
const { serverId: currentServerId, worldId, server } = modalServerContext
const currentUserId = ref<string | null>(null)
const currentUserRole = ref<string | null>(null)
const serverFull = ref<Archon.Servers.v1.ServerFull | null>(null)
const isApp = ref(true)
const serverInstanceSettingsTabComponentMap = {
general: ServerInstanceSettingsGeneralPage,
installation: ServerSettingsInstallationPage,
properties: ServerSettingsPropertiesPage,
advanced: ServerInstanceSettingsAdvancedPage,
} as const
provideServerSettings({
isApp,
currentUserId,
currentUserRole,
browseModpacks: props.browseModpacks ?? (() => {}),
closeModal: () => hide(),
})
const ownerId = computed(() => server.value?.owner_id ?? 'Ghost')
const isOwner = computed(() => currentUserId.value != null && currentUserId.value === ownerId.value)
const isAdmin = computed(() => currentUserRole.value === 'admin')
const currentInstanceName = computed(() => {
const id = worldId.value
if (!id) return null
return serverFull.value?.worlds.find((world) => world.id === id)?.name ?? null
})
const tabs = computed<TabbedModalTab[]>(() =>
serverInstanceSettingsTabDefinitions.map((tab) => {
const ctx = {
serverId: currentServerId,
ownerId: ownerId.value,
serverStatus: server.value?.status,
isOwner: isOwner.value,
isAdmin: isAdmin.value,
}
const name = defineMessage({
id: `server.instance-settings.tabs.${tab.id}`,
defaultMessage: tab.label,
})
const shown = tab.shown ? tab.shown(ctx) : true
return {
name,
icon: tab.icon,
content: serverInstanceSettingsTabComponentMap[tab.id],
shown,
}
}),
)
async function fetchViewer() {
currentUserId.value = null
currentUserRole.value = null
const result = await props.resolveViewer()
currentUserId.value = result.userId
currentUserRole.value = result.userRole
}
async function show({ serverId, tabIndex, tabId, worldId: requestedWorldId }: ShowOptions) {
try {
const targetServerId = currentServerId
selectedWorldId.value = requestedWorldId ?? baseServerContext.worldId.value
if (serverId !== targetServerId) {
console.warn(
`[ServerInstanceSettingsModal] Ignoring mismatched serverId "${serverId}" in favor of context "${targetServerId}"`,
)
}
const cachedServer = queryClient.getQueryData<Archon.Servers.v0.Server>([
'servers',
'detail',
targetServerId,
])
const cachedFull = queryClient.getQueryData<Archon.Servers.v1.ServerFull>([
'servers',
'v1',
'detail',
targetServerId,
])
serverFull.value = cachedFull ?? null
modal.value?.show()
const visibleTabs = tabs.value.filter((tab) => tab.shown !== false)
let requestedTab = tabIndex ?? 0
if (tabId) {
const defIndex = serverInstanceSettingsTabDefinitions.findIndex((d) => d.id === tabId)
if (defIndex >= 0) {
const visibleIndex = visibleTabs.findIndex(
(_, i) => tabs.value.indexOf(visibleTabs[i]) === defIndex,
)
if (visibleIndex >= 0) requestedTab = visibleIndex
}
}
const clampedTab = Math.min(Math.max(requestedTab, 0), Math.max(visibleTabs.length - 1, 0))
nextTick(() => modal.value?.setTab(clampedTab))
const fetchPromises: Promise<unknown>[] = [fetchViewer()]
if (!cachedServer) {
fetchPromises.push(
queryClient.fetchQuery({
queryKey: ['servers', 'detail', targetServerId],
queryFn: () => client.archon.servers_v0.get(targetServerId),
}),
)
}
if (!cachedFull) {
fetchPromises.push(
queryClient
.fetchQuery({
queryKey: ['servers', 'v1', 'detail', targetServerId],
queryFn: () => client.archon.servers_v1.get(targetServerId),
})
.then((data) => {
serverFull.value = data
}),
)
}
await Promise.all(fetchPromises)
if (worldId.value) {
queryClient.prefetchQuery({
queryKey: ['servers', 'properties', 'v1', targetServerId, worldId.value],
queryFn: () => client.archon.properties_v1.getProperties(targetServerId, worldId.value!),
})
queryClient.prefetchQuery({
queryKey: ['content', 'list', 'v1', targetServerId, worldId.value],
queryFn: () =>
client.archon.content_v1.getAddons(targetServerId, worldId.value!, {
from_modpack: false,
}),
})
queryClient.prefetchQuery({
queryKey: ['servers', 'startup', 'v1', targetServerId, worldId.value],
queryFn: () => client.archon.options_v1.getStartup(targetServerId, worldId.value!),
})
}
} catch (error) {
console.error(error)
addNotification({
type: 'error',
title: formatMessage(messages.failedToLoadServer),
})
}
}
function hide() {
modal.value?.hide()
}
defineExpose({ show, hide })
</script>
<template>
<TabbedModal
ref="modal"
:tabs="tabs"
:max-width="'min(980px, calc(95vw - 2rem))'"
:width="'min(980px, calc(95vw - 2rem))'"
>
<template #title>
<span class="flex min-w-0 items-center gap-2 text-lg font-semibold text-primary">
<span class="truncate">{{ server.name || 'Server' }}</span>
<ChevronRightIcon class="shrink-0" />
<span class="truncate">{{ currentInstanceName || 'Instance' }}</span>
<ChevronRightIcon class="shrink-0" />
<span class="shrink-0 font-extrabold text-contrast">{{
formatMessage(commonMessages.settingsLabel)
}}</span>
</span>
</template>
</TabbedModal>
</template>
@@ -10,9 +10,7 @@ import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n'
import {
ServerSettingsAdvancedPage,
ServerSettingsGeneralPage,
ServerSettingsInstallationPage,
ServerSettingsNetworkPage,
ServerSettingsPropertiesPage,
serverSettingsTabDefinitions,
type ServerSettingsTabId,
} from '#ui/layouts/shared/server-settings'
@@ -53,7 +51,7 @@ const messages = defineMessages({
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
const { serverId: currentServerId, worldId, server } = injectModrinthServerContext()
const { serverId: currentServerId, server } = injectModrinthServerContext()
const currentUserId = ref<string | null>(null)
const currentUserRole = ref<string | null>(null)
@@ -72,9 +70,7 @@ useQuery({
const serverSettingsTabComponentMap = {
general: ServerSettingsGeneralPage,
installation: ServerSettingsInstallationPage,
network: ServerSettingsNetworkPage,
properties: ServerSettingsPropertiesPage,
advanced: ServerSettingsAdvancedPage,
} as const
@@ -146,12 +142,6 @@ async function show({ serverId, tabIndex, tabId }: ShowOptions) {
'detail',
targetServerId,
])
const cachedFull = queryClient.getQueryData<Archon.Servers.v1.ServerFull>([
'servers',
'v1',
'detail',
targetServerId,
])
modal.value?.show()
const visibleTabs = tabs.value.filter((tab) => tab.shown !== false)
@@ -179,34 +169,8 @@ async function show({ serverId, tabIndex, tabId }: ShowOptions) {
)
}
if (!cachedFull) {
fetchPromises.push(
queryClient.fetchQuery({
queryKey: ['servers', 'v1', 'detail', targetServerId],
queryFn: () => client.archon.servers_v1.get(targetServerId),
}),
)
}
await Promise.all(fetchPromises)
if (worldId.value) {
queryClient.prefetchQuery({
queryKey: ['servers', 'properties', 'v1', targetServerId, worldId.value],
queryFn: () => client.archon.properties_v1.getProperties(targetServerId, worldId.value!),
})
queryClient.prefetchQuery({
queryKey: ['content', 'list', 'v1', targetServerId, worldId.value],
queryFn: () =>
client.archon.content_v1.getAddons(targetServerId, worldId.value!, {
from_modpack: false,
}),
})
queryClient.prefetchQuery({
queryKey: ['servers', 'startup', 'v1', targetServerId, worldId.value],
queryFn: () => client.archon.options_v1.getStartup(targetServerId, worldId.value!),
})
}
} catch (error) {
console.error(error)
addNotification({
@@ -41,7 +41,7 @@ const { formatMessage } = useVIntl()
const messages = defineMessages({
rateLimitTitle: {
id: 'servers.setup.rate-limit.title',
defaultMessage: 'Cannot reinstall server',
defaultMessage: 'Cannot reinstall instance',
},
rateLimitText: {
id: 'servers.setup.rate-limit.text',
@@ -118,7 +118,7 @@ const messages = defineMessages({
creatingBackupDescription: {
id: 'servers.backups.admonition.creating-backup.description',
defaultMessage:
'Saving world data and server configuration for {backupName}. This can take a few minutes.',
'Saving instance data and server configuration for {backupName}. This can take a few minutes.',
},
backupFailedTitle: {
id: 'servers.backups.admonition.backup-failed.title',
@@ -11,6 +11,7 @@ export { default as ModrinthServersIcon } from './ModrinthServersIcon.vue'
export { default as SaveBanner } from './SaveBanner.vue'
export * from './server-header'
export { default as ServerListEmpty } from './server-list-empty/ServerListEmpty.vue'
export { default as ServerInstanceSettingsModal } from './ServerInstanceSettingsModal.vue'
export { type PendingChange, default as ServerListing } from './ServerListing.vue'
export { default as ServerSettingsModal } from './ServerSettingsModal.vue'
export { default as ServerSetupModal } from './ServerSetupModal.vue'
@@ -146,7 +146,9 @@
</span>
</AutoLink>
</div>
<span v-else class="font-semibold text-contrast">{{ formatMessage(messages.noModpack) }}</span>
<span v-else class="font-semibold text-contrast">{{
formatMessage(messages.noModpack)
}}</span>
</div>
<div
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,25%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
@@ -17,7 +17,7 @@
</AutoLink>
<div
v-else
v-tooltip="'Change server version'"
v-tooltip="'Change instance version'"
class="pointer-events-none flex min-w-0 flex-row items-center gap-1 truncate text-sm font-medium"
>
{{ game[0].toUpperCase() + game.slice(1) }}
@@ -46,7 +46,7 @@ defineProps<{
const settingsModal = injectServerSettingsModal(null)
const settingsLinkTarget = computed(() => {
if (settingsModal) {
return () => settingsModal.openServerSettings({ tabId: 'installation' })
return () => settingsModal.openServerInstanceSettings({ tabId: 'installation' })
}
return ''
})
@@ -6,7 +6,7 @@
<div v-else class="size-5 shrink-0 animate-pulse rounded-full bg-button-border"></div>
<AutoLink
v-if="isLink"
v-tooltip="'Change server loader'"
v-tooltip="'Change instance loader'"
:to="settingsLinkTarget"
class="flex min-w-0 items-center font-medium text-sm"
:class="settingsLinkTarget ? 'hover:underline' : ''"
@@ -54,7 +54,7 @@ defineProps<{
const settingsModal = injectServerSettingsModal(null)
const settingsLinkTarget = computed(() => {
if (settingsModal) {
return () => settingsModal.openServerSettings({ tabId: 'installation' })
return () => settingsModal.openServerInstanceSettings({ tabId: 'installation' })
}
return ''
})
@@ -15,6 +15,7 @@
:backup-name="
visibleBackupTip ? `Before bulk update (${visibleBackupTip})` : 'Before bulk update'
"
:target-type="props.targetType"
:shift-click-hint-override="formatMessage(messages.shiftClickHint)"
@update:buttons-disabled="buttonsDisabled = $event"
/>
@@ -88,6 +89,7 @@ const props = defineProps<{
backupTip?: string
actionDisabled?: boolean
actionDisabledTooltip?: string
targetType?: 'server' | 'instance'
}>()
const emit = defineEmits<{
@@ -17,6 +17,7 @@
<InlineBackupCreator
ref="backupCreator"
:backup-name="props.backupTip ? `Before deletion (${props.backupTip})` : 'Before deletion'"
:target-type="props.targetType"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -75,7 +76,7 @@ const messages = defineMessages({
admonitionBody: {
id: 'content.confirm-deletion.admonition-body',
defaultMessage:
'Deleting a mod can permanently affect your world and may cause missing content or unexpected issues when it loads again.',
'Deleting a mod can permanently affect your instance and may cause missing content or unexpected issues when it starts again.',
},
deleteButton: {
id: 'content.confirm-deletion.delete-button',
@@ -91,12 +92,14 @@ const props = withDefaults(
backupTip?: string
actionDisabled?: boolean
actionDisabledTooltip?: string
targetType?: 'server' | 'instance'
}>(),
{
variant: 'instance',
backupTip: undefined,
actionDisabled: false,
actionDisabledTooltip: undefined,
targetType: undefined,
},
)
@@ -22,6 +22,7 @@
<InlineBackupCreator
ref="backupCreator"
:backup-name="backupName"
:target-type="props.targetType"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -68,6 +69,7 @@ const props = defineProps<{
backupTip?: string
actionDisabled?: boolean
actionDisabledTooltip?: string
targetType?: 'server' | 'instance'
}>()
const { formatMessage } = useVIntl()
@@ -13,6 +13,7 @@
<InlineBackupCreator
ref="backupCreator"
:backup-name="backupTip ? `Before reinstall (${backupTip})` : 'Before reinstall'"
:target-type="targetType"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -75,6 +76,7 @@ const messages = defineMessages({
defineProps<{
server?: boolean
backupTip?: string
targetType?: 'server' | 'instance'
}>()
const emit = defineEmits<{
@@ -13,6 +13,7 @@
<InlineBackupCreator
ref="backupCreator"
:backup-name="props.backupTip ? `Before unlink (${props.backupTip})` : 'Before unlink'"
:target-type="props.targetType"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -58,6 +59,7 @@ const props = defineProps<{
backupTip?: string
actionDisabled?: boolean
actionDisabledTooltip?: string
targetType?: 'server' | 'instance'
}>()
const { formatMessage } = useVIntl()
@@ -1,11 +1,7 @@
<template>
<div class="flex flex-col gap-3">
<span class="text-primary">
{{
formatMessage(messages.warningBody, {
type: formatMessage(backup.isServer ? messages.worldLabel : messages.instanceLabel),
})
}}
{{ formatMessage(messages.warningBody, { type: backupTargetType }) }}
</span>
<div v-if="backup.available" class="flex items-center gap-2">
@@ -47,7 +43,7 @@
<TriangleAlertIcon
v-if="backup.isServer"
v-tooltip="formatMessage(messages.backupTakesAWhile)"
v-tooltip="formatMessage(messages.backupTakesAWhile, { type: backupTargetType })"
class="size-5 shrink-0 text-brand-orange hover:brightness-110"
/>
</div>
@@ -71,6 +67,7 @@ import { useInlineBackup } from '../../composables/use-inline-backup'
const props = defineProps<{
backupName: string
targetType?: 'server' | 'instance'
hideShiftClickHint?: boolean
shiftClickHintOverride?: string
}>()
@@ -87,6 +84,7 @@ const canManageBackups = computed(
const permissionDeniedMessage = computed(() => formatMessage(commonMessages.noPermissionAction))
const backup = useInlineBackup(() => props.backupName)
const backupTargetType = computed(() => props.targetType ?? 'instance')
function startBackup() {
if (
@@ -115,15 +113,7 @@ const messages = defineMessages({
warningBody: {
id: 'content.inline-backup.warning-body',
defaultMessage:
'We recommend creating a backup before proceeding so you can restore your {type} if anything breaks.',
},
worldLabel: {
id: 'content.inline-backup.world-label',
defaultMessage: 'world',
},
instanceLabel: {
id: 'content.inline-backup.instance-label',
defaultMessage: 'instance',
'We recommend creating a backup before proceeding so you can restore your {type, select, server {server} other {instance}} if anything breaks.',
},
createBackup: {
id: 'content.inline-backup.create-backup',
@@ -144,7 +134,7 @@ const messages = defineMessages({
backupTakesAWhile: {
id: 'content.inline-backup.backup-takes-a-while',
defaultMessage:
'Creating a backup may take several minutes depending on the size of your server.',
'Creating a backup may take several minutes depending on the size of your {type, select, server {server} other {instance}}.',
},
backupInProgress: {
id: 'content.inline-backup.backup-in-progress',
@@ -207,9 +207,10 @@
formatMessage(
incompatibilityWarningMode
? messages.incompatibilityWarning
: isApp
? messages.updateWarningApp
: messages.updateWarningWeb,
: messages.updateWarning,
{
type: updateWarningTargetType,
},
)
}}</span>
</div>
@@ -352,14 +353,10 @@ const messages = defineMessages({
id: 'instances.updater-modal.select-version',
defaultMessage: 'Select a version to view its changelog',
},
updateWarningApp: {
id: 'instances.updater-modal.warning-app',
updateWarning: {
id: 'instances.updater-modal.warning',
defaultMessage:
'Updating can break your instance. Review version changelogs and back up first.',
},
updateWarningWeb: {
id: 'instances.updater-modal.warning-web',
defaultMessage: 'Updating can break your world. Review version changelogs and back up first.',
'Updating can break your {type, select, server {server} other {instance}}. Review version changelogs and back up first.',
},
incompatibilityWarning: {
id: 'instances.updater-modal.incompatibility-warning',
@@ -424,6 +421,7 @@ const props = withDefaults(
currentLoader: string
currentVersionId: string
isApp: boolean
targetType?: 'server' | 'instance'
/** The project type (e.g. mod, shader, resourcepack, datapack, modpack). */
projectType?: string
projectIconUrl?: string
@@ -440,6 +438,7 @@ const props = withDefaults(
actionDisabledTooltip?: string
}>(),
{
targetType: undefined,
projectType: undefined,
projectIconUrl: undefined,
projectName: undefined,
@@ -469,6 +468,7 @@ const defaultHeader = computed(() => {
: messages.updateVersionHeader,
)
})
const updateWarningTargetType = computed(() => props.targetType ?? 'instance')
const emit = defineEmits<{
update: [version: Labrinth.Versions.v2.Version, event: MouseEvent]
@@ -1039,6 +1039,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:backup-tip="pendingDeletionItems.map((i) => i.project?.title ?? i.file_name).join(', ')"
:action-disabled="ctx.isBusy.value"
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
:target-type="ctx.deletionContext ?? 'instance'"
@delete="confirmDelete"
/>
<ContentDependencyWarningModal
@@ -1059,6 +1060,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:server="ctx.deletionContext === 'server'"
:action-disabled="ctx.isBusy.value"
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
:target-type="ctx.deletionContext ?? 'instance'"
@update="confirmBulkUpdate"
/>
<ConfirmUnlinkModal
@@ -1068,6 +1070,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:backup-tip="ctx.modpack.value?.project.title"
:action-disabled="ctx.isBusy.value"
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
:target-type="ctx.deletionContext ?? 'instance'"
@unlink="ctx.unlinkModpack!()"
/>
@@ -5,7 +5,13 @@
<Admonition :type="hasUnknownContent ? 'warning' : 'info'" :header="admonitionHeader">
<div class="flex flex-col gap-2">
<span>{{ description }}</span>
<span v-if="hasUnknownContent">{{ formatMessage(messages.unknownContentBody) }}</span>
<span v-if="hasUnknownContent">
{{
formatMessage(messages.unknownContentBody, {
type: targetType ?? 'server',
})
}}
</span>
</div>
</Admonition>
@@ -73,6 +79,7 @@
<InlineBackupCreator
ref="backupCreator"
backup-name="Before version change"
:target-type="targetType"
hide-shift-click-hint
@update:buttons-disabled="buttonsDisabled = $event"
/>
@@ -131,6 +138,7 @@ const props = defineProps<{
confirmIcon?: Component
showReportButton?: boolean
showBackupCreator?: boolean
targetType?: 'server' | 'instance'
removedLabel?: string
disableClose?: boolean
}>()
@@ -192,7 +200,7 @@ const messages = defineMessages({
unknownContentBody: {
id: 'content.diff-modal.unknown-content-body',
defaultMessage:
'Some content on your server could not be analyzed and may be affected by this change.',
'Some content on your {type, select, server {server} other {instance}} could not be analyzed and may be affected by this change.',
},
})
@@ -13,7 +13,9 @@
<span>
{{
variant === 'loader-change'
? formatMessage(messages.loaderChangeBody)
? formatMessage(messages.loaderChangeBody, {
type: server ? 'server' : 'instance',
})
: formatMessage(messages.gameVersionWarningBody)
}}
</span>
@@ -21,7 +23,11 @@
<ButtonStyled color="red">
<button :disabled="loading" @click="handleResetServer">
<TrashIcon class="size-5" />
{{ formatMessage(commonMessages.resetServerButton) }}
{{
formatMessage(messages.resetButton, {
type: server ? 'server' : 'instance',
})
}}
</button>
</ButtonStyled>
</div>
@@ -33,6 +39,7 @@
:backup-name="
variant === 'loader-change' ? 'Before loader change' : 'Before version change'
"
:target-type="server ? 'server' : 'instance'"
hide-shift-click-hint
@update:buttons-disabled="buttonsDisabled = $event"
/>
@@ -104,6 +111,7 @@ import InlineBackupCreator from '../../content-tab/components/modals/InlineBacku
defineProps<{
variant: 'loader-change' | 'game-version-change'
loading?: boolean
server?: boolean
}>()
const emit = defineEmits<{
@@ -165,7 +173,7 @@ const messages = defineMessages({
loaderChangeBody: {
id: 'installation-settings.incompatible-content.loader-change-body',
defaultMessage:
'When changing the loader, all installed content will be disabled. We recommend resetting your server instead.',
'When changing the loader, all installed content will be disabled. We recommend resetting your {type, select, server {server} other {instance}} instead.',
},
gameVersionWarningTitle: {
id: 'installation-settings.incompatible-content.game-version-warning-title',
@@ -188,6 +196,10 @@ const messages = defineMessages({
id: 'installation-settings.incompatible-content.disable-conflicts-button',
defaultMessage: 'Disable conflicts',
},
resetButton: {
id: 'installation-settings.incompatible-content.reset-button',
defaultMessage: 'Reset {type, select, server {server} other {instance}}',
},
})
defineExpose({ show, hide })
@@ -189,7 +189,9 @@ const platformDisabledItems = computed(() =>
const platformDisabledTooltip = computed(() =>
ctx.isBusy.value
? (ctx.busyMessage?.value ?? undefined)
: formatMessage(messages.platformLockTooltip),
: formatMessage(messages.platformLockTooltip, {
type: ctx.isServer ? 'server' : 'instance',
}),
)
const showModpackVersionActions = computed(() => {
@@ -206,6 +208,19 @@ const isLocalFile = computed(() => {
const isLinkedModpack = computed(() => showModpackVersionActions.value || isLocalFile.value)
const showBackupCreator = computed(() => {
const val = ctx.showBackupCreator
if (val == null) return ctx.isServer
return typeof val === 'boolean' ? val : val.value
})
const repairDescriptionMessage = computed(() => {
const kind = ctx.repairDescriptionKind ?? (ctx.isServer ? 'server' : 'app-instance')
if (kind === 'server') return messages.repairServerDescription
if (kind === 'server-instance') return messages.repairServerInstanceDescription
return messages.repairInstanceDescription
})
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event?: MouseEvent) {
debug('handleModpackUpdateRequest: start', {
versionId: version.id,
@@ -468,6 +483,11 @@ const messages = defineMessages({
defaultMessage:
'Reinstalls the loader and Minecraft dependencies without deleting your content. This may resolve issues if your server is not starting correctly.',
},
repairServerInstanceDescription: {
id: 'installation-settings.repair.server-instance-description',
defaultMessage:
'Reinstalls the loader and Minecraft dependencies without deleting your content. This may resolve issues if this instance is not starting correctly.',
},
editWarningInstance: {
id: 'installation-settings.edit.warning-instance',
defaultMessage:
@@ -540,7 +560,8 @@ const messages = defineMessages({
},
platformLockTooltip: {
id: 'installation-settings.platform-lock-tooltip',
defaultMessage: 'You will need to reset your server to switch loader.',
defaultMessage:
'You will need to reset your {type, select, server {server} other {instance}} to switch loader.',
},
confirmVersionChangeHeader: {
id: 'installation-settings.confirm-version-change-header',
@@ -552,7 +573,8 @@ const messages = defineMessages({
},
confirmVersionChangeDescription: {
id: 'installation-settings.confirm-version-change-description',
defaultMessage: 'Changing to {gameVersion} will modify the following content on your server.',
defaultMessage:
'Changing to {gameVersion} will modify the following content on your {type, select, server {server} other {instance}}.',
},
removedIncompatible: {
id: 'installation-settings.removed-incompatible',
@@ -771,13 +793,7 @@ const messages = defineMessages({
</ButtonStyled>
</div>
<span class="text-primary">
{{
formatMessage(
ctx.isServer
? messages.repairServerDescription
: messages.repairInstanceDescription,
)
}}
{{ formatMessage(repairDescriptionMessage) }}
</span>
</div>
</template>
@@ -1009,13 +1025,7 @@ const messages = defineMessages({
</ButtonStyled>
</div>
<span class="text-primary">
{{
formatMessage(
ctx.isServer
? messages.repairServerDescription
: messages.repairInstanceDescription,
)
}}
{{ formatMessage(repairDescriptionMessage) }}
</span>
</div>
</template>
@@ -1073,6 +1083,7 @@ const messages = defineMessages({
ref="incompatibleContentModal"
:variant="form.incompatibleContentVariant.value"
:loading="form.isVerifying.value || form.isSaving.value"
:server="ctx.isServer"
@confirm-loader-change="form.confirmLoaderChange()"
@auto-fix="form.confirmAutoFix()"
@disable-conflicts="form.confirmDisableConflicts()"
@@ -1087,6 +1098,7 @@ const messages = defineMessages({
:description="
formatMessage(messages.confirmVersionChangeDescription, {
gameVersion: form.pendingPreview.value.newGameVersion,
type: ctx.isServer ? 'server' : 'instance',
})
"
:admonition-header="formatMessage(messages.confirmVersionChangeHeader)"
@@ -1094,8 +1106,9 @@ const messages = defineMessages({
:has-unknown-content="form.pendingPreview.value.hasUnknownContent"
:confirm-label="formatMessage(messages.confirmVersionChange)"
:confirm-icon="SaveIcon"
:target-type="ctx.isServer ? 'server' : 'instance'"
:removed-label="formatMessage(messages.removedIncompatible)"
:show-backup-creator="ctx.isServer"
:show-backup-creator="showBackupCreator"
@confirm="form.confirmSave()"
@cancel="form.cancelPreview()"
/>
@@ -63,6 +63,12 @@ export interface InstallationSettingsContext {
/** True when the linked modpack was uploaded as a local file rather than from Modrinth */
isLocalFile?: boolean | ComputedRef<boolean>
/** Controls whether destructive installation changes offer inline backups. Defaults to isServer. */
showBackupCreator?: boolean | ComputedRef<boolean>
/** Controls repair description copy separately from the title target. */
repairDescriptionKind?: 'server' | 'server-instance' | 'app-instance'
repairing?: Ref<boolean>
reinstalling?: Ref<boolean>
@@ -2,7 +2,6 @@
<div class="relative h-full w-full">
<div class="flex h-full w-full flex-col gap-4">
<div class="flex flex-col gap-6">
<!-- SFTP section -->
<div class="flex flex-col gap-2">
<div class="flex flex-col items-center justify-between gap-0.5 sm:flex-row">
<span class="text-lg font-semibold text-contrast">SFTP</span>
@@ -89,7 +88,6 @@
:disabled="!canWriteFiles"
@click.stop="togglePasswordVisibility"
>
<!-- look into doing stop propagation here -->
<EyeIcon v-if="showPassword" class="h-5 w-5" />
<EyeOffIcon v-else class="h-5 w-5" />
</button>
@@ -100,156 +98,28 @@
</div>
</div>
</div>
<!-- Startup command section -->
<div class="flex flex-col gap-2.5">
<div class="flex h-10 flex-col items-end justify-between gap-4 sm:flex-row">
<label for="startup-command-field" class="mb-0.5 flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast">Startup command</span>
</label>
<ButtonStyled v-if="startupCommand !== defaultStartupCommand" type="transparent">
<button
v-tooltip="advancedActionTooltip"
:disabled="
isStartupLoading ||
startupCommand === defaultStartupCommand ||
!canUseAdvancedSettings
"
class="relative !w-full sm:!w-auto"
@click="resetToDefault"
>
<UpdatedIcon class="h-5 w-5" />
Default
</button>
</ButtonStyled>
</div>
<div class="relative">
<StyledInput
id="startup-command-field"
v-model="startupCommand"
v-tooltip="advancedActionTooltip"
multiline
resize="vertical"
input-class="font-mono field-sizing-content"
:disabled="isStartupLoading || !canUseAdvancedSettings"
/>
<div
v-if="isStartupLoading"
class="bg-bg/50 absolute inset-0 flex items-center justify-center rounded-xl"
>
<SpinnerIcon class="h-6 w-6 animate-spin text-secondary" />
</div>
</div>
<span> The command that runs when your server is started. </span>
</div>
<!-- Java version section -->
<div class="flex flex-col gap-2.5">
<div class="flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast">Java version</span>
</div>
<div class="relative max-w-xs">
<Combobox
:id="'java-version-field'"
v-model="javaVersion"
v-tooltip="advancedActionTooltip"
name="java-version"
:options="displayedJavaVersions"
:display-value="javaVersionLabel ?? 'Java Version'"
:disabled="isStartupLoading || !canUseAdvancedSettings"
>
<template #dropdown-footer>
<button
class="flex w-full cursor-pointer items-center justify-center gap-1.5 border-0 border-t border-solid border-surface-5 bg-transparent py-3 text-center text-sm font-semibold text-secondary transition-colors hover:text-contrast"
@mousedown.prevent
@click="showAllVersions = !showAllVersions"
>
<EyeOffIcon v-if="showAllVersions" class="size-4" />
<EyeIcon v-else class="size-4" />
{{ showAllVersions ? 'Hide extra versions' : 'Show all versions' }}
</button>
</template>
</Combobox>
<div
v-if="isStartupLoading"
class="bg-bg/50 absolute inset-0 flex items-center justify-center rounded-xl"
>
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
</div>
</div>
<span> The Java version your server runs on. </span>
</div>
<!-- Java runtime section -->
<div class="flex flex-col gap-2.5">
<div class="flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast">Java runtime</span>
</div>
<div class="relative max-w-xs">
<Combobox
:id="'runtime-field'"
v-model="jreVendor"
v-tooltip="advancedActionTooltip"
name="runtime"
:options="JRE_VENDORS"
:display-value="jreVendorLabel ?? 'Runtime'"
:disabled="isStartupLoading || !canUseAdvancedSettings"
/>
<div
v-if="isStartupLoading"
class="bg-bg/50 absolute inset-0 flex items-center justify-center rounded-xl"
>
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
</div>
</div>
<span> The Java runtime your server will use. </span>
</div>
</div>
</div>
<SaveBanner
:is-visible="!!hasUnsavedChanges || isPending"
:server-id="serverId"
:is-updating="isPending"
:save="saveStartup"
:reset="resetStartup"
/>
</div>
</template>
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import {
CopyIcon,
ExternalIcon,
EyeIcon,
EyeOffIcon,
SpinnerIcon,
UpdatedIcon,
} from '@modrinth/assets'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { ButtonStyled, Combobox, StyledInput } from '#ui/components'
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
import { ButtonStyled } from '#ui/components'
import { useServerPermissions } from '#ui/composables/server-permissions'
import {
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
} from '#ui/providers'
const { addNotification } = injectNotificationManager()
const { server, serverId, worldId } = injectModrinthServerContext()
const client = injectModrinthClient()
const queryClient = useQueryClient()
const { canUseAdvancedSettings, canWriteFiles, permissionDeniedMessage } = useServerPermissions()
const { server } = injectModrinthServerContext()
const { canWriteFiles, permissionDeniedMessage } = useServerPermissions()
// SFTP state
const showPassword = ref(false)
const sftpUrl = computed(() => `sftp://${server.value?.sftp_username}@${server.value?.sftp_host}`)
const advancedActionTooltip = computed(() =>
canUseAdvancedSettings.value ? undefined : permissionDeniedMessage.value,
)
const sftpActionTooltip = computed(() =>
canWriteFiles.value
? 'This button only works with compatible SFTP clients (e.g. WinSCP)'
@@ -272,160 +142,10 @@ const copyToClipboard = (name: string, textToCopy?: string) => {
})
}
// Startup state
const startupQueryKey = computed(() => ['servers', 'startup', 'v1', serverId, worldId.value])
const { data: startupData, isLoading: isStartupLoading } = useQuery({
queryKey: startupQueryKey,
queryFn: () => client.archon.options_v1.getStartup(serverId, worldId.value!),
enabled: computed(() => worldId.value !== null),
})
function togglePasswordVisibility() {
if (!canWriteFiles.value) return
showPassword.value = !showPassword.value
}
const JAVA_VERSIONS = [
{ value: 8, label: 'Java 8' },
{ value: 11, label: 'Java 11' },
{ value: 17, label: 'Java 17' },
{ value: 21, label: 'Java 21' },
{ value: 25, label: 'Java 25' },
]
const showAllVersions = ref(false)
type MinecraftReleaseVersion = {
major: number
minor: number
}
function parseMinecraftReleaseVersion(version: string): MinecraftReleaseVersion | null {
const [majorPart, minorPart] = version.split('.')
if (!majorPart || !minorPart) return null
const major = Number(majorPart)
const minor = Number(minorPart)
if (!Number.isInteger(major) || !Number.isInteger(minor)) return null
return { major, minor }
}
function filterJavaVersions(compatibleVersions: number[]) {
return JAVA_VERSIONS.filter((version) => compatibleVersions.includes(version.value))
}
const displayedJavaVersions = computed(() => {
if (showAllVersions.value) return JAVA_VERSIONS
const mcVersion = server.value?.mc_version ?? ''
if (!mcVersion) return JAVA_VERSIONS
const releaseVersion = parseMinecraftReleaseVersion(mcVersion)
if (!releaseVersion) return JAVA_VERSIONS
if (releaseVersion.major > 1) {
if (releaseVersion.major >= 26) {
return filterJavaVersions([25])
}
return JAVA_VERSIONS
}
if (releaseVersion.minor >= 20) return filterJavaVersions([21])
if (releaseVersion.minor >= 17) return filterJavaVersions([17, 21])
if (releaseVersion.minor >= 12) return filterJavaVersions([8, 11, 17, 21])
if (releaseVersion.minor >= 6) return filterJavaVersions([8, 11])
return filterJavaVersions([8])
})
const JRE_VENDORS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
{ value: 'corretto', label: 'Corretto' },
{ value: 'temurin', label: 'Temurin' },
{ value: 'graal', label: 'GraalVM' },
]
const savedStartupCommand = computed(() => startupData.value?.startup_command ?? '')
const savedJavaVersion = computed(() => startupData.value?.java_version ?? undefined)
const savedJreVendor = computed(() => startupData.value?.jre_vendor ?? undefined)
const defaultStartupCommand = computed(
() => startupData.value?.original_invocation ?? savedStartupCommand.value,
)
const startupCommand = ref('')
const javaVersion = ref<number>()
const jreVendor = ref<Archon.Content.v1.JreVendor>()
const javaVersionLabel = computed(
() => JAVA_VERSIONS.find((v) => v.value === javaVersion.value)?.label,
)
const jreVendorLabel = computed(() => JRE_VENDORS.find((v) => v.value === jreVendor.value)?.label)
function syncFormFromData() {
startupCommand.value = savedStartupCommand.value
javaVersion.value = savedJavaVersion.value
jreVendor.value = savedJreVendor.value
}
watch(
startupData,
(newData, oldData) => {
if (newData && !oldData) {
syncFormFromData()
}
},
{ immediate: true },
)
const hasUnsavedChanges = computed(
() =>
startupCommand.value !== savedStartupCommand.value ||
javaVersion.value !== savedJavaVersion.value ||
jreVendor.value !== savedJreVendor.value,
)
const { mutate: saveStartupMutation, isPending } = useMutation({
mutationFn: () =>
client.archon.options_v1.patchStartup(serverId, worldId.value!, {
startup_command: startupCommand.value || null,
java_version: javaVersion.value ?? null,
jre_vendor: jreVendor.value ?? null,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: startupQueryKey.value })
syncFormFromData()
addNotification({
type: 'success',
title: 'Server settings updated',
text: 'Your server settings were successfully changed.',
})
},
onError: (error) => {
console.error(error)
addNotification({
type: 'error',
title: 'Failed to update server arguments',
text: 'Please try again later.',
})
},
})
function saveStartup() {
if (!canUseAdvancedSettings.value) return
saveStartupMutation()
}
function resetStartup() {
syncFormFromData()
}
function resetToDefault() {
if (!canUseAdvancedSettings.value) return
startupCommand.value = defaultStartupCommand.value
}
</script>
<style scoped>
@@ -1,5 +1,7 @@
export { default as ServerSettingsAdvancedPage } from './advanced.vue'
export { default as ServerSettingsGeneralPage } from './general.vue'
export { default as ServerSettingsInstallationPage } from './installation.vue'
export { default as ServerInstanceSettingsAdvancedPage } from './instance-advanced.vue'
export { default as ServerInstanceSettingsGeneralPage } from './instance-general.vue'
export { default as ServerSettingsNetworkPage } from './network.vue'
export { default as ServerSettingsPropertiesPage } from './properties.vue'
@@ -16,30 +16,6 @@
ref="installationSettingsLayout"
@reset-server="showResetServerModal"
>
<template #extra>
<div class="flex flex-col gap-2.5">
<span class="text-lg font-semibold text-contrast">{{
formatMessage(messages.resetServerTitle)
}}</span>
<div>
<ButtonStyled color="red">
<button
v-tooltip="resetServerDisabledTooltip"
class="!shadow-none"
:disabled="resetServerDisabled"
@click="showResetServerModal"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(commonMessages.resetServerButton) }}
</button>
</ButtonStyled>
</div>
<span class="text-primary">
{{ formatMessage(messages.resetServerDescription) }}
</span>
</div>
</template>
<template #extra-modals>
<Teleport to="body">
<div class="relative z-[100]">
@@ -121,15 +97,6 @@ const uploadProgressModal =
useTemplateRef<InstanceType<typeof UploadProgressModal>>('uploadProgressModal')
const messages = defineMessages({
resetServerTitle: {
id: 'hosting.loader.reset-server',
defaultMessage: 'Reset server',
},
resetServerDescription: {
id: 'hosting.loader.reset-server-description',
defaultMessage:
'Removes all data on your server, including your worlds, mods, and configuration files. Backups will remain and can be restored.',
},
loaderVersionLabel: {
id: 'hosting.loader.loader-version',
defaultMessage: '{loader, select, null {Loader} other {{loader}}} version',
@@ -152,11 +119,11 @@ const messages = defineMessages({
},
repairStartedText: {
id: 'hosting.loader.repair-started-text',
defaultMessage: 'Your server installation has been repaired.',
defaultMessage: 'Your instance installation has been repaired.',
},
failedToRepair: {
id: 'hosting.loader.failed-to-repair',
defaultMessage: 'Failed to repair server',
defaultMessage: 'Failed to repair instance',
},
failedToReinstall: {
id: 'hosting.loader.failed-to-reinstall',
@@ -181,19 +148,19 @@ const messages = defineMessages({
resetToOnboardingModalDescription: {
id: 'hosting.loader.reset-to-onboarding-modal-description',
defaultMessage:
'This will send the server back into onboarding so setup can be completed again. Are you sure you want to continue?',
'This will send the instance back into onboarding so setup can be completed again. Are you sure you want to continue?',
},
resetToOnboardingSuccessTitle: {
id: 'hosting.loader.reset-to-onboarding-success-title',
defaultMessage: 'Server reset to onboarding',
defaultMessage: 'Instance reset to onboarding',
},
resetToOnboardingSuccessDescription: {
id: 'hosting.loader.reset-to-onboarding-success-description',
defaultMessage: 'The server has been returned to the onboarding flow.',
defaultMessage: 'The instance has been returned to the onboarding flow.',
},
failedToResetToOnboarding: {
id: 'hosting.loader.failed-to-reset-to-onboarding',
defaultMessage: 'Failed to reset server to onboarding',
defaultMessage: 'Failed to reset instance to onboarding',
},
})
@@ -221,10 +188,6 @@ const setupActionDisabledMessage = computed(() => {
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : null
})
const resetServerDisabled = computed(() => !canResetServer.value || isInstalling.value)
const resetServerDisabledTooltip = computed(() => {
if (!canResetServer.value) return permissionDeniedMessage.value
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined
})
const installationSettingsLayout = ref<InstanceType<typeof InstallationSettingsLayout>>()
const setupModal = ref<InstanceType<typeof ServerSetupModal>>()
const contentListQueryKey = computed(() => ['content', 'list', 'v1', serverId, worldId.value])
@@ -268,8 +231,24 @@ const modpackVersionsQuery = useQuery({
const isSiteAdmin = computed(() => serverSettings.currentUserRole.value === 'admin')
const editingPlatform = ref(server.value?.loader?.toLowerCase() ?? 'vanilla')
const editingGameVersion = ref(server.value?.mc_version ?? '')
function normalizeLoader(loader?: string | null) {
const normalized = loader?.toLowerCase()
if (!normalized) return 'vanilla'
if (normalized === 'neo_forge') return 'neoforge'
return normalized
}
const currentPlatform = computed(() =>
normalizeLoader(addonsQuery.data.value?.modloader ?? server.value?.loader),
)
const currentGameVersion = computed(
() => addonsQuery.data.value?.game_version ?? server.value?.mc_version ?? '',
)
const currentLoaderVersion = computed(
() => addonsQuery.data.value?.modloader_version ?? server.value?.loader_version ?? '',
)
const editingPlatform = ref(currentPlatform.value)
const editingGameVersion = ref(currentGameVersion.value)
const resetToOnboardingModal = ref<InstanceType<typeof ConfirmModal>>()
const isResettingToOnboarding = ref(false)
const supportResetToOnboardingDisabled = computed(
@@ -456,9 +435,9 @@ provideInstallationSettings({
: undefined,
}
}),
currentPlatform: computed(() => server.value?.loader?.toLowerCase() ?? 'vanilla'),
currentGameVersion: computed(() => server.value?.mc_version ?? ''),
currentLoaderVersion: computed(() => server.value?.loader_version ?? ''),
currentPlatform,
currentGameVersion,
currentLoaderVersion,
availablePlatforms: ['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur'],
editingPlatformRef: editingPlatform,
@@ -540,11 +519,10 @@ provideInstallationSettings({
async save(platform, gameVersion, loaderVersionId) {
if (setupActionDisabled.value) return
debug('save: called with', { platform, gameVersion, loaderVersionId })
const currentPlatform = server.value?.loader?.toLowerCase() ?? 'vanilla'
const platformChanged = platform !== currentPlatform
const gameVersionChanged = gameVersion !== (server.value?.mc_version ?? '')
const platformChanged = platform !== currentPlatform.value
const gameVersionChanged = gameVersion !== currentGameVersion.value
const loaderVersionChanged =
loaderVersionId !== null && loaderVersionId !== (server.value?.loader_version ?? '')
loaderVersionId !== null && loaderVersionId !== currentLoaderVersion.value
let resolvedLoaderVersion = loaderVersionId
if (!resolvedLoaderVersion && platform !== 'vanilla') {
@@ -776,10 +754,12 @@ provideInstallationSettings({
currentLoader: addonsQuery.data.value?.modloader ?? server.value?.loader ?? '',
})),
isServer: true,
isServer: false,
isApp: serverSettings.isApp.value,
showModpackVersionActions: computed(() => modpack.value?.spec.platform === 'modrinth'),
isLocalFile: computed(() => modpack.value?.spec.platform === 'local_file'),
showBackupCreator: true,
repairDescriptionKind: 'server-instance',
lockPlatform: false,
hideLoaderVersion: false,
@@ -896,8 +876,21 @@ watch(
})
if (oldStatus === 'installing' && newStatus === 'available') {
debug('status installing->available, resetting editing refs')
editingPlatform.value = server.value?.loader?.toLowerCase() ?? 'vanilla'
editingGameVersion.value = server.value?.mc_version ?? ''
editingPlatform.value = currentPlatform.value
editingGameVersion.value = currentGameVersion.value
}
},
)
watch(
[worldId, currentPlatform, currentGameVersion],
([, newPlatform, newGameVersion], [, oldPlatform, oldGameVersion]) => {
if (
editingPlatform.value === oldPlatform &&
editingGameVersion.value === oldGameVersion
) {
editingPlatform.value = newPlatform
editingGameVersion.value = newGameVersion
}
},
)
@@ -0,0 +1,274 @@
<template>
<div class="relative h-full w-full">
<div class="flex h-full w-full flex-col gap-4">
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<div class="flex h-10 flex-col items-end justify-between gap-4 sm:flex-row">
<label for="startup-command-field" class="mb-0.5 flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast">Startup command</span>
</label>
<ButtonStyled v-if="startupCommand !== defaultStartupCommand" type="transparent">
<button
:disabled="isStartupLoading || startupCommand === defaultStartupCommand"
class="relative !w-full sm:!w-auto"
@click="resetToDefault"
>
<UpdatedIcon class="h-5 w-5" />
Default
</button>
</ButtonStyled>
</div>
<div class="relative">
<StyledInput
id="startup-command-field"
v-model="startupCommand"
multiline
resize="vertical"
input-class="font-mono field-sizing-content"
:disabled="isStartupLoading"
/>
<div
v-if="isStartupLoading"
class="bg-bg/50 absolute inset-0 flex items-center justify-center rounded-xl"
>
<SpinnerIcon class="h-6 w-6 animate-spin text-secondary" />
</div>
</div>
<span>The command that runs when your instance is started.</span>
</div>
<div class="flex flex-col gap-2.5">
<div class="flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast">Java version</span>
</div>
<div class="relative max-w-xs">
<Combobox
:id="'java-version-field'"
v-model="javaVersion"
name="java-version"
:options="displayedJavaVersions"
:display-value="javaVersionLabel ?? 'Java Version'"
:disabled="isStartupLoading"
>
<template #dropdown-footer>
<button
class="flex w-full cursor-pointer items-center justify-center gap-1.5 border-0 border-t border-solid border-surface-5 bg-transparent py-3 text-center text-sm font-semibold text-secondary transition-colors hover:text-contrast"
@mousedown.prevent
@click="showAllVersions = !showAllVersions"
>
<EyeOffIcon v-if="showAllVersions" class="size-4" />
<EyeIcon v-else class="size-4" />
{{ showAllVersions ? 'Hide extra versions' : 'Show all versions' }}
</button>
</template>
</Combobox>
<div
v-if="isStartupLoading"
class="bg-bg/50 absolute inset-0 flex items-center justify-center rounded-xl"
>
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
</div>
</div>
<span>
The Java version your instance runs on. By default, only versions compatible with
your Minecraft version are shown.
</span>
</div>
<div class="flex flex-col gap-2.5">
<div class="flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast">Java runtime</span>
</div>
<div class="relative max-w-xs">
<Combobox
:id="'runtime-field'"
v-model="jreVendor"
name="runtime"
:options="JRE_VENDORS"
:display-value="jreVendorLabel ?? 'Runtime'"
:disabled="isStartupLoading"
/>
<div
v-if="isStartupLoading"
class="bg-bg/50 absolute inset-0 flex items-center justify-center rounded-xl"
>
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
</div>
</div>
<span>The Java runtime your instance will use.</span>
</div>
</div>
</div>
<SaveBanner
:is-visible="!!hasUnsavedChanges || isPending"
:server-id="serverId"
:is-updating="isPending"
:save="() => saveStartup()"
:reset="resetStartup"
/>
</div>
</template>
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { EyeIcon, EyeOffIcon, SpinnerIcon, UpdatedIcon } from '@modrinth/assets'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { ButtonStyled, Combobox, StyledInput } from '#ui/components'
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
import {
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
} from '#ui/providers'
const { addNotification } = injectNotificationManager()
const { server, serverId, worldId } = injectModrinthServerContext()
const client = injectModrinthClient()
const queryClient = useQueryClient()
const startupQueryKey = computed(() => ['servers', 'startup', 'v1', serverId, worldId.value])
const { data: startupData, isLoading: isStartupLoading } = useQuery({
queryKey: startupQueryKey,
queryFn: () => client.archon.options_v1.getStartup(serverId, worldId.value!),
enabled: computed(() => worldId.value !== null),
})
const JAVA_VERSIONS = [
{ value: 8, label: 'Java 8' },
{ value: 11, label: 'Java 11' },
{ value: 17, label: 'Java 17' },
{ value: 21, label: 'Java 21' },
{ value: 25, label: 'Java 25' },
]
const showAllVersions = ref(false)
type MinecraftReleaseVersion = {
major: number
minor: number
}
function parseMinecraftReleaseVersion(version: string): MinecraftReleaseVersion | null {
const [majorPart, minorPart] = version.split('.')
if (!majorPart || !minorPart) return null
const major = Number(majorPart)
const minor = Number(minorPart)
if (!Number.isInteger(major) || !Number.isInteger(minor)) return null
return { major, minor }
}
function filterJavaVersions(compatibleVersions: number[]) {
return JAVA_VERSIONS.filter((version) => compatibleVersions.includes(version.value))
}
const displayedJavaVersions = computed(() => {
if (showAllVersions.value) return JAVA_VERSIONS
// TODO: Use the selected instance's content Minecraft version instead of the server fallback.
const mcVersion = server.value?.mc_version ?? ''
if (!mcVersion) return JAVA_VERSIONS
const releaseVersion = parseMinecraftReleaseVersion(mcVersion)
if (!releaseVersion) return JAVA_VERSIONS
if (releaseVersion.major > 1) {
if (releaseVersion.major >= 26) {
return filterJavaVersions([25])
}
return JAVA_VERSIONS
}
if (releaseVersion.minor >= 20) return filterJavaVersions([21])
if (releaseVersion.minor >= 17) return filterJavaVersions([17, 21])
if (releaseVersion.minor >= 12) return filterJavaVersions([8, 11, 17, 21])
if (releaseVersion.minor >= 6) return filterJavaVersions([8, 11])
return filterJavaVersions([8])
})
const JRE_VENDORS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
{ value: 'corretto', label: 'Corretto' },
{ value: 'temurin', label: 'Temurin' },
{ value: 'graal', label: 'GraalVM' },
]
const savedStartupCommand = computed(() => startupData.value?.startup_command ?? '')
const savedJavaVersion = computed(() => startupData.value?.java_version ?? undefined)
const savedJreVendor = computed(() => startupData.value?.jre_vendor ?? undefined)
const defaultStartupCommand = computed(
() => startupData.value?.original_invocation ?? savedStartupCommand.value,
)
const startupCommand = ref('')
const javaVersion = ref<number>()
const jreVendor = ref<Archon.Content.v1.JreVendor>()
const javaVersionLabel = computed(
() => JAVA_VERSIONS.find((v) => v.value === javaVersion.value)?.label,
)
const jreVendorLabel = computed(() => JRE_VENDORS.find((v) => v.value === jreVendor.value)?.label)
function syncFormFromData() {
startupCommand.value = savedStartupCommand.value
javaVersion.value = savedJavaVersion.value
jreVendor.value = savedJreVendor.value
}
watch(
startupData,
(newData, oldData) => {
if (newData && !oldData) {
syncFormFromData()
}
},
{ immediate: true },
)
const hasUnsavedChanges = computed(
() =>
startupCommand.value !== savedStartupCommand.value ||
javaVersion.value !== savedJavaVersion.value ||
jreVendor.value !== savedJreVendor.value,
)
const { mutate: saveStartup, isPending } = useMutation({
mutationFn: () =>
client.archon.options_v1.patchStartup(serverId, worldId.value!, {
startup_command: startupCommand.value || null,
java_version: javaVersion.value ?? null,
jre_vendor: jreVendor.value ?? null,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: startupQueryKey.value })
syncFormFromData()
addNotification({
type: 'success',
title: 'Instance settings updated',
text: 'Your instance settings were successfully changed.',
})
},
onError: (error) => {
console.error(error)
addNotification({
type: 'error',
title: 'Failed to update instance arguments',
text: 'Please try again later.',
})
},
})
function resetStartup() {
syncFormFromData()
}
function resetToDefault() {
startupCommand.value = defaultStartupCommand.value
}
</script>
@@ -0,0 +1,171 @@
<template>
<div class="flex h-full w-full flex-col gap-6">
<Teleport to="body">
<div class="relative z-[100]">
<ServerSetupModal
ref="setupModal"
@reinstall="onResetEverything"
@browse-modpacks="onBrowseModpacks"
/>
</div>
</Teleport>
<div class="flex max-w-[496px] flex-col gap-2.5">
<label for="instance-name-field" class="font-semibold text-contrast">
{{ formatMessage(messages.instanceNameLabel) }}
</label>
<div v-tooltip="formatMessage(messages.notImplemented)" class="max-w-[400px]">
<StyledInput
id="instance-name-field"
:model-value="instanceName"
disabled
wrapper-class="w-full"
/>
</div>
<span class="text-primary">{{ formatMessage(messages.instanceNameDescription) }}</span>
</div>
<div class="flex flex-col gap-2.5">
<span class="font-semibold text-contrast">
{{ formatMessage(messages.dangerZoneLabel) }}
</span>
<div class="flex flex-col gap-4 rounded-2xl border border-solid border-surface-5 p-4">
<div class="flex flex-col items-start gap-2.5">
<div v-tooltip="formatMessage(messages.notImplemented)">
<ButtonStyled color="red">
<button class="!shadow-none" disabled>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(messages.resetWorldFilesButton) }}
</button>
</ButtonStyled>
</div>
<span class="text-primary">
{{ formatMessage(messages.resetWorldFilesDescription) }}
</span>
</div>
<div class="flex flex-col items-start gap-2.5">
<ButtonStyled color="red">
<button class="!shadow-none" :disabled="isResetDisabled" @click="setupModal?.show()">
<TrashIcon class="size-5" />
{{ formatMessage(messages.resetEverythingButton) }}
</button>
</ButtonStyled>
<span class="text-primary">
{{ formatMessage(messages.resetEverythingDescription) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { RotateCounterClockwiseIcon, TrashIcon } from '@modrinth/assets'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { ButtonStyled, StyledInput } from '#ui/components'
import ServerSetupModal from '#ui/components/servers/ServerSetupModal.vue'
import { useModrinthServersConsole } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectServerSettings } from '#ui/layouts/shared/server-settings'
import { injectModrinthClient, injectModrinthServerContext } from '#ui/providers'
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const { server, serverId, worldId, isSyncingContent, busyReasons } = injectModrinthServerContext()
const serverSettings = injectServerSettings()
const queryClient = useQueryClient()
const modrinthServersConsole = useModrinthServersConsole()
const setupModal = ref<InstanceType<typeof ServerSetupModal>>()
const messages = defineMessages({
instanceNameLabel: {
id: 'server.instance-settings.general.instance-name.label',
defaultMessage: 'Instance name',
},
instanceNameDescription: {
id: 'server.instance-settings.general.instance-name.description',
defaultMessage: 'This name is only visible on Modrinth.',
},
instanceFallbackName: {
id: 'server.instance-settings.general.instance-name.fallback',
defaultMessage: 'Instance',
},
dangerZoneLabel: {
id: 'server.instance-settings.general.danger-zone.label',
defaultMessage: 'Danger zone',
},
resetWorldFilesButton: {
id: 'server.instance-settings.general.reset-world-files.button',
defaultMessage: 'Reset world data',
},
resetWorldFilesDescription: {
id: 'server.instance-settings.general.reset-world-files.description',
defaultMessage:
'Delete the current world data and generate a new one. Your content and files will stay the same.',
},
resetEverythingButton: {
id: 'server.instance-settings.general.reset-everything.button',
defaultMessage: 'Reset everything',
},
resetEverythingDescription: {
id: 'server.instance-settings.general.reset-everything.description',
defaultMessage:
'Reset your instance completely. This removes world data, content, and any configuration. A backup of the previous instance will remain available.',
},
notImplemented: {
id: 'server.instance-settings.general.not-implemented',
defaultMessage: 'Not yet implemented',
},
})
const serverFullQuery = useQuery({
queryKey: ['servers', 'v1', 'detail', serverId],
queryFn: () => client.archon.servers_v1.get(serverId),
staleTime: 30_000,
})
const currentWorld = computed(() => {
const id = worldId.value
if (!id) return null
return serverFullQuery.data.value?.worlds.find((world) => world.id === id) ?? null
})
const instanceName = computed(
() => currentWorld.value?.name ?? formatMessage(messages.instanceFallbackName),
)
const isResetDisabled = computed(
() =>
!worldId.value ||
server.value?.status === 'installing' ||
isSyncingContent.value ||
busyReasons.value.length > 0,
)
function onResetEverything() {
modrinthServersConsole.clear()
queryClient.removeQueries({ queryKey: ['servers', 'ws-state', serverId] })
void Promise.all([
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] }),
worldId.value
? queryClient.invalidateQueries({
queryKey: ['content', 'list', 'v1', serverId, worldId.value],
})
: Promise.resolve(),
])
serverSettings.closeModal?.()
}
function onBrowseModpacks() {
serverSettings.browseModpacks({
serverId,
worldId: worldId.value,
from: 'reset-server',
})
}
</script>
@@ -10,14 +10,11 @@ import {
} from '@modrinth/assets'
import type { Component } from 'vue'
export type ServerSettingsTabId =
| 'general'
| 'installation'
| 'network'
| 'properties'
| 'advanced'
| 'billing'
| 'admin-billing'
export type ServerSettingsTabId = 'general' | 'network' | 'advanced' | 'billing' | 'admin-billing'
export type ServerInstanceSettingsTabId = 'general' | 'installation' | 'properties' | 'advanced'
export type AnyServerSettingsTabId = ServerSettingsTabId | ServerInstanceSettingsTabId
export interface ServerSettingsTabContext {
serverId: string
@@ -36,28 +33,24 @@ export interface ServerSettingsTabDefinition {
shown?: (ctx: ServerSettingsTabContext) => boolean
}
export interface ServerInstanceSettingsTabDefinition {
id: ServerInstanceSettingsTabId
label: string
icon: Component
shown?: (ctx: ServerSettingsTabContext) => boolean
}
export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
{
id: 'general',
label: 'General',
icon: SettingsIcon,
},
{
id: 'installation',
label: 'Installation',
icon: WrenchIcon,
},
{
id: 'network',
label: 'Network',
icon: VersionIcon,
},
{
id: 'properties',
label: 'Properties',
icon: ListIcon,
shown: ({ serverStatus }) => serverStatus !== 'installing',
},
{
id: 'advanced',
label: 'Advanced',
@@ -80,3 +73,27 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
shown: ({ isAdmin }) => isAdmin,
},
]
export const serverInstanceSettingsTabDefinitions: ServerInstanceSettingsTabDefinition[] = [
{
id: 'general',
label: 'General',
icon: SettingsIcon,
},
{
id: 'installation',
label: 'Installation',
icon: WrenchIcon,
},
{
id: 'properties',
label: 'Properties',
icon: ListIcon,
shown: ({ serverStatus }) => serverStatus !== 'installing',
},
{
id: 'advanced',
label: 'Advanced',
icon: TextQuoteIcon,
},
]
@@ -221,7 +221,7 @@
<ButtonStyled color="red" type="standard">
<button
class="whitespace-pre"
@click="openServerSettingsModal('installation')"
@click="openServerInstanceSettingsModal('installation')"
>
<RightArrowIcon />
{{ formatMessage(messages.changeLoader) }}
@@ -289,6 +289,13 @@
:browse-modpacks="handleBrowseModpacks"
/>
</Suspense>
<Suspense>
<ServerInstanceSettingsModal
ref="serverInstanceSettingsModal"
:resolve-viewer="resolveViewer"
:browse-modpacks="handleBrowseModpacks"
/>
</Suspense>
<ConfirmLeaveModal
ref="confirmLeaveModal"
:header="formatMessage(leaveMessages.uploadInProgress)"
@@ -328,6 +335,7 @@ import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
import { ServerManageHeader } from '#ui/components/servers/server-header'
import ServerInstanceSettingsModal from '#ui/components/servers/ServerInstanceSettingsModal.vue'
import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue'
import {
hasServerPermission,
@@ -343,7 +351,10 @@ import { useServerPanelSync } from '#ui/composables/server-panel-sync'
import { useServerBackupsQueue } from '#ui/composables/servers/server-backups-queue.ts'
import { useServerManageCoreRuntime } from '#ui/composables/servers/server-manage-core-runtime.ts'
import type { LogLine } from '#ui/layouts/shared/console'
import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings'
import type {
ServerInstanceSettingsTabId,
ServerSettingsTabId,
} from '#ui/layouts/shared/server-settings'
import {
injectModrinthClient,
injectNotificationManager,
@@ -714,6 +725,9 @@ function dismissInstancesHint() {
}
const serverSettingsModal = ref<InstanceType<typeof ServerSettingsModal> | null>(null)
const serverInstanceSettingsModal = ref<InstanceType<typeof ServerInstanceSettingsModal> | null>(
null,
)
const confirmLeaveModal = ref<InstanceType<typeof ConfirmLeaveModal>>()
const {
@@ -1490,6 +1504,18 @@ function openServerSettingsModal(tabId?: ServerSettingsTabId) {
serverSettingsModal.value?.show({ serverId: props.serverId, tabId })
}
function openServerInstanceSettingsModal(
tabId?: ServerInstanceSettingsTabId,
targetWorldId?: string | null,
) {
if (!props.serverId) return
serverInstanceSettingsModal.value?.show({
serverId: props.serverId,
tabId,
worldId: targetWorldId,
})
}
function handleBrowseModpacks(args: {
serverId: string
worldId: string | null
@@ -1508,6 +1534,8 @@ function handleBrowseContent(args: {
provideServerSettingsModal({
openServerSettings: (options) => openServerSettingsModal(options?.tabId),
openServerInstanceSettings: (options) =>
openServerInstanceSettingsModal(options?.tabId, options?.worldId),
browseServerContent: (args) => handleBrowseContent(args),
})
@@ -1675,12 +1703,23 @@ onMounted(() => {
}
if (route.query.openSettings) {
const tabId = route.query.openSettings as ServerSettingsTabId
const tabId =
typeof route.query.openSettings === 'string' ? route.query.openSettings : undefined
router.replace({ query: { ...route.query, openSettings: undefined } })
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', props.serverId] })
queryClient.invalidateQueries({ queryKey: ['servers', 'startup', 'v1', props.serverId] })
nextTick(() => openServerSettingsModal(tabId))
if (tabId === 'installation' || tabId === 'properties') {
nextTick(() => openServerInstanceSettingsModal(tabId))
} else if (
tabId === 'general' ||
tabId === 'network' ||
tabId === 'advanced' ||
tabId === 'billing' ||
tabId === 'admin-billing'
) {
nextTick(() => openServerSettingsModal(tabId))
}
}
})
@@ -284,9 +284,9 @@ import BackupDeleteModal from '#ui/components/servers/backups/BackupDeleteModal.
import BackupItem from '#ui/components/servers/backups/BackupItem.vue'
import BackupRenameModal from '#ui/components/servers/backups/BackupRenameModal.vue'
import BackupRestoreModal from '#ui/components/servers/backups/BackupRestoreModal.vue'
import { useBackupsSelection } from '#ui/composables/servers/backups-selection'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { useServerPermissions } from '#ui/composables/server-permissions'
import { useBackupsSelection } from '#ui/composables/servers/backups-selection'
import { useServerBackupsQueue } from '#ui/composables/servers/server-backups-queue.ts'
import { useBulkOperation } from '#ui/layouts/shared/content-tab/composables/bulk-operations'
import {
@@ -124,7 +124,7 @@ const contentUploadSession = useUploadSessionUpload({
cancelUpload,
})
const { addNotification } = injectNotificationManager()
const { openServerSettings, browseServerContent } = injectServerSettingsModal()
const { openServerInstanceSettings, browseServerContent } = injectServerSettingsModal()
const { canSetup, permissionDeniedMessage } = useServerPermissions()
const router = useRouter()
const queryClient = useQueryClient()
@@ -1345,14 +1345,14 @@ provideContentManager({
},
browse: handleBrowseContent,
uploadFiles: handleUploadFiles,
deletionContext: 'server',
deletionContext: 'instance',
hasUpdateSupport: true,
updateItem: handleUpdateItem,
bulkUpdateItems: handleBulkUpdate,
updateModpack: handleModpackUpdate,
viewModpackContent: handleViewModpackContent,
unlinkModpack: handleModpackUnlink,
openSettings: () => openServerSettings({ tabId: 'installation' }),
openSettings: () => openServerInstanceSettings({ tabId: 'installation' }),
switchVersion: handleSwitchVersion,
getOverflowOptions,
getItemId: getContentItemId,
@@ -1386,9 +1386,9 @@ provideContentManager({
<template #modals>
<ConfirmUnlinkModal
ref="modpackUnlinkModal"
server
:action-disabled="setupActionDisabled"
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
target-type="instance"
@unlink="handleModpackUnlinkConfirm"
/>
<ModpackContentModal
@@ -1430,6 +1430,7 @@ provideContentManager({
:loading-changelog="loadingChangelog"
:action-disabled="setupActionDisabled"
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
target-type="instance"
@update="handleModalUpdate"
@cancel="resetUpdateState"
@version-select="handleVersionSelect"
@@ -1446,9 +1447,9 @@ provideContentManager({
.filter(Boolean)
.join(' ')
"
server
:action-disabled="setupActionDisabled"
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
target-type="instance"
@confirm="handleModpackUpdateConfirm"
@cancel="handleModpackUpdateCancel"
/>
@@ -88,7 +88,7 @@ const messages = defineMessages({
const client = injectModrinthClient()
const { serverId, server, worldId, isServerRunning } = injectModrinthServerContext()
const { openServerSettings } = injectServerSettingsModal()
const { openServerInstanceSettings } = injectServerSettingsModal()
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const router = useRouter()
@@ -253,7 +253,7 @@ const headerActions = computed(() => [
icon: Settings2Icon,
labelHidden: true,
tooltip: formatMessage(messages.instanceSettings),
onClick: () => openServerSettings({ tabId: 'installation' }),
onClick: () => openServerInstanceSettings({ tabId: 'general' }),
},
])
@@ -124,7 +124,7 @@ const INSTANCE_INFO_ADMONITION_KEY = 'server-instances-info-admonition-dismissed
const client = injectModrinthClient()
const { serverId, server, isServerRunning } = injectModrinthServerContext()
const { openServerSettings } = injectServerSettingsModal()
const { openServerInstanceSettings } = injectServerSettingsModal()
const { formatMessage } = useVIntl()
const router = useRouter()
const instanceInfoAdmonitionDismissed = useStorage(INSTANCE_INFO_ADMONITION_KEY, false)
@@ -322,12 +322,12 @@ function handleEditWorld(worldId: string) {
)
}
function handleWorldSettings() {
openServerSettings({ tabId: 'installation' })
function handleWorldSettings(worldId: string) {
openServerInstanceSettings({ tabId: 'general', worldId })
}
function handleCreateWorld() {
openServerSettings({ tabId: 'installation' })
openServerInstanceSettings({ tabId: 'installation' })
}
function dismissInstanceInfoAdmonition() {
+62 -44
View File
@@ -35,6 +35,9 @@
"affiliate.viewAnalytics": {
"defaultMessage": "View analytics"
},
"app.server-instance-settings.failed-to-load-server": {
"defaultMessage": "Failed to load instance settings"
},
"app.server-settings.failed-to-load-server": {
"defaultMessage": "Failed to load server settings"
},
@@ -345,7 +348,7 @@
"defaultMessage": "Update {count, plural, one {# project} other {# projects}}"
},
"content.confirm-deletion.admonition-body": {
"defaultMessage": "Deleting a mod can permanently affect your world and may cause missing content or unexpected issues when it loads again."
"defaultMessage": "Deleting a mod can permanently affect your instance and may cause missing content or unexpected issues when it starts again."
},
"content.confirm-deletion.admonition-header": {
"defaultMessage": "Deletion warning"
@@ -438,7 +441,7 @@
"defaultMessage": "{count} removed"
},
"content.diff-modal.unknown-content-body": {
"defaultMessage": "Some content on your server could not be analyzed and may be affected by this change."
"defaultMessage": "Some content on your {type, select, server {server} other {instance}} could not be analyzed and may be affected by this change."
},
"content.diff-modal.updated-count": {
"defaultMessage": "{count} updated"
@@ -468,22 +471,16 @@
"defaultMessage": "A backup is in progress, it's recommended to wait for it to finish before performing this action."
},
"content.inline-backup.backup-takes-a-while": {
"defaultMessage": "Creating a backup may take several minutes depending on the size of your server."
"defaultMessage": "Creating a backup may take several minutes depending on the size of your {type, select, server {server} other {instance}}."
},
"content.inline-backup.create-backup": {
"defaultMessage": "Create backup"
},
"content.inline-backup.instance-label": {
"defaultMessage": "instance"
},
"content.inline-backup.shift-click-hint": {
"defaultMessage": "Hold Shift while clicking to skip confirmation."
},
"content.inline-backup.warning-body": {
"defaultMessage": "We recommend creating a backup before proceeding so you can restore your {type} if anything breaks."
},
"content.inline-backup.world-label": {
"defaultMessage": "world"
"defaultMessage": "We recommend creating a backup before proceeding so you can restore your {type, select, server {server} other {instance}} if anything breaks."
},
"content.modpack-card.content-hint-description": {
"defaultMessage": "Your modpack's content can now be found here!"
@@ -687,7 +684,7 @@
"defaultMessage": "Additional settings"
},
"creation-flow.modal.final-config.backup.before-reset-server.name": {
"defaultMessage": "Before reset server"
"defaultMessage": "Before reset instance"
},
"creation-flow.modal.final-config.difficulty.easy": {
"defaultMessage": "Easy"
@@ -876,7 +873,7 @@
"defaultMessage": "Import instance"
},
"creation-flow.title.reset-server": {
"defaultMessage": "Reset server"
"defaultMessage": "Reset instance"
},
"creation-flow.title.set-up-server": {
"defaultMessage": "Set up server"
@@ -1692,10 +1689,10 @@
"defaultMessage": "Failed to reinstall modpack"
},
"hosting.loader.failed-to-repair": {
"defaultMessage": "Failed to repair server"
"defaultMessage": "Failed to repair instance"
},
"hosting.loader.failed-to-reset-to-onboarding": {
"defaultMessage": "Failed to reset server to onboarding"
"defaultMessage": "Failed to reset instance to onboarding"
},
"hosting.loader.failed-to-save-settings": {
"defaultMessage": "Failed to save installation settings"
@@ -1707,31 +1704,25 @@
"defaultMessage": "{loader, select, null {Loader} other {{loader}}} version"
},
"hosting.loader.repair-started-text": {
"defaultMessage": "Your server installation has been repaired."
"defaultMessage": "Your instance installation has been repaired."
},
"hosting.loader.repair-started-title": {
"defaultMessage": "Repair completed"
},
"hosting.loader.reset-server": {
"defaultMessage": "Reset server"
},
"hosting.loader.reset-server-description": {
"defaultMessage": "Removes all data on your server, including your worlds, mods, and configuration files. Backups will remain and can be restored."
},
"hosting.loader.reset-to-onboarding-button": {
"defaultMessage": "Reset to onboarding"
},
"hosting.loader.reset-to-onboarding-modal-description": {
"defaultMessage": "This will send the server back into onboarding so setup can be completed again. Are you sure you want to continue?"
"defaultMessage": "This will send the instance back into onboarding so setup can be completed again. Are you sure you want to continue?"
},
"hosting.loader.reset-to-onboarding-modal-title": {
"defaultMessage": "Reset to onboarding"
},
"hosting.loader.reset-to-onboarding-success-description": {
"defaultMessage": "The server has been returned to the onboarding flow."
"defaultMessage": "The instance has been returned to the onboarding flow."
},
"hosting.loader.reset-to-onboarding-success-title": {
"defaultMessage": "Server reset to onboarding"
"defaultMessage": "Instance reset to onboarding"
},
"hosting.loader.support-options-title": {
"defaultMessage": "Support options"
@@ -1794,7 +1785,7 @@
"defaultMessage": "Confirm"
},
"installation-settings.confirm-version-change-description": {
"defaultMessage": "Changing to {gameVersion} will modify the following content on your server."
"defaultMessage": "Changing to {gameVersion} will modify the following content on your {type, select, server {server} other {instance}}."
},
"installation-settings.confirm-version-change-header": {
"defaultMessage": "Review content changes"
@@ -1827,11 +1818,14 @@
"defaultMessage": "Incompatible content installed"
},
"installation-settings.incompatible-content.loader-change-body": {
"defaultMessage": "When changing the loader, all installed content will be disabled. We recommend resetting your server instead."
"defaultMessage": "When changing the loader, all installed content will be disabled. We recommend resetting your {type, select, server {server} other {instance}} instead."
},
"installation-settings.incompatible-content.loader-change-title": {
"defaultMessage": "Changing loaders is destructive"
},
"installation-settings.incompatible-content.reset-button": {
"defaultMessage": "Reset {type, select, server {server} other {instance}}"
},
"installation-settings.linked-instance.title": {
"defaultMessage": "Linked {projectType}"
},
@@ -1845,7 +1839,7 @@
"defaultMessage": "{loader} version"
},
"installation-settings.platform-lock-tooltip": {
"defaultMessage": "You will need to reset your server to switch loader."
"defaultMessage": "You will need to reset your {type, select, server {server} other {instance}} to switch loader."
},
"installation-settings.reinstall-modpack.description": {
"defaultMessage": "Re-installing the modpack resets the {type} content to its original state, removing any mods or content you have added."
@@ -1868,6 +1862,9 @@
"installation-settings.repair.server-description": {
"defaultMessage": "Reinstalls the loader and Minecraft dependencies without deleting your content. This may resolve issues if your server is not starting correctly."
},
"installation-settings.repair.server-instance-description": {
"defaultMessage": "Reinstalls the loader and Minecraft dependencies without deleting your content. This may resolve issues if this instance is not starting correctly."
},
"installation-settings.repair.server-title": {
"defaultMessage": "Repair server"
},
@@ -2075,11 +2072,8 @@
"instances.updater-modal.update-to": {
"defaultMessage": "Update to {version}"
},
"instances.updater-modal.warning-app": {
"defaultMessage": "Updating can break your instance. Review version changelogs and back up first."
},
"instances.updater-modal.warning-web": {
"defaultMessage": "Updating can break your world. Review version changelogs and back up first."
"instances.updater-modal.warning": {
"defaultMessage": "Updating can break your {type, select, server {server} other {instance}}. Review version changelogs and back up first."
},
"label.actions": {
"defaultMessage": "Actions"
@@ -3896,6 +3890,33 @@
"servers.access-table.user-avatar-alt": {
"defaultMessage": "{username}'s avatar"
},
"server.instance-settings.general.danger-zone.label": {
"defaultMessage": "Danger zone"
},
"server.instance-settings.general.instance-name.description": {
"defaultMessage": "This name is only visible on Modrinth."
},
"server.instance-settings.general.instance-name.fallback": {
"defaultMessage": "Instance"
},
"server.instance-settings.general.instance-name.label": {
"defaultMessage": "Instance name"
},
"server.instance-settings.general.not-implemented": {
"defaultMessage": "Not yet implemented"
},
"server.instance-settings.general.reset-everything.button": {
"defaultMessage": "Reset everything"
},
"server.instance-settings.general.reset-everything.description": {
"defaultMessage": "Reset your instance completely. This removes world data, content, and any configuration. A backup of the previous instance will remain available."
},
"server.instance-settings.general.reset-world-files.button": {
"defaultMessage": "Reset world data"
},
"server.instance-settings.general.reset-world-files.description": {
"defaultMessage": "Delete the current world data and generate a new one. Your content and files will stay the same."
},
"servers.admonitions.background-task-running": {
"defaultMessage": "Background task running"
},
@@ -4179,7 +4200,7 @@
"defaultMessage": "Backup timed out"
},
"servers.backups.admonition.creating-backup.description": {
"defaultMessage": "Saving world data and server configuration for {backupName}. This can take a few minutes."
"defaultMessage": "Saving instance data and server configuration for {backupName}. This can take a few minutes."
},
"servers.backups.admonition.creating-backup.title": {
"defaultMessage": "Creating backup"
@@ -4700,8 +4721,8 @@
"servers.manage.instances.card.last-active": {
"defaultMessage": "Last active"
},
"servers.manage.instances.card.none": {
"defaultMessage": "None"
"servers.manage.instances.card.no-modpack": {
"defaultMessage": ""
},
"servers.manage.instances.card.not-tracked-yet": {
"defaultMessage": "Not tracked yet"
@@ -4709,17 +4730,14 @@
"servers.manage.instances.card.settings": {
"defaultMessage": "Instance settings"
},
"servers.manage.instances.info.definition": {
"defaultMessage": "An instance is a separate server setup."
"servers.manage.instances.info.body": {
"defaultMessage": "An instance is a separate setup of your server with its own content, files, worlds, and settings. You can switch which instance your server runs at any time."
},
"servers.manage.instances.info.files": {
"defaultMessage": "Each instance has its own server files, worlds, installed content, and settings."
"servers.manage.instances.info.dismiss": {
"defaultMessage": "Don't show this again"
},
"servers.manage.instances.info.header": {
"defaultMessage": "What is an instance?"
},
"servers.manage.instances.info.switching": {
"defaultMessage": "You can switch which instance your server runs."
"defaultMessage": "What is a server instance?"
},
"servers.manage.instances.slot-name": {
"defaultMessage": "Instance #{index}"
@@ -5091,7 +5109,7 @@
"defaultMessage": "You are being rate limited. Please try again later."
},
"servers.setup.rate-limit.title": {
"defaultMessage": "Cannot reinstall server"
"defaultMessage": "Cannot reinstall instance"
},
"servers.setup.reinstall-failed.text": {
"defaultMessage": "An unexpected error occurred while reinstalling. Please try again later."
@@ -1,4 +1,7 @@
import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings'
import type {
ServerInstanceSettingsTabId,
ServerSettingsTabId,
} from '#ui/layouts/shared/server-settings'
import { createContext } from './create-context'
@@ -10,6 +13,10 @@ export interface BrowseServerContentArgs {
export interface ServerSettingsModalContext {
openServerSettings: (options?: { tabId?: ServerSettingsTabId }) => void
openServerInstanceSettings: (options?: {
tabId?: ServerInstanceSettingsTabId
worldId?: string | null
}) => void
browseServerContent?: (args: BrowseServerContentArgs) => void | Promise<void>
}