feat: implement improved flow for server panel edit installation (#5711)
* feat: implement improved flow for server panel edit installation * feat: installation form finalized * feat: error state for InstallingBanner * feat: action button refactor + save banner text fix * fix: lint * fix: content card alignment * feat: better copy * fix: lint * fix: hide shift click + fix NeoForge chip * fix: lint
This commit is contained in:
@@ -4,19 +4,19 @@
|
|||||||
<div class="flex flex-col gap-4 md:w-[400px]">
|
<div class="flex flex-col gap-4 md:w-[400px]">
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Are you sure you want to
|
Are you sure you want to
|
||||||
<span class="lowercase">{{ confirmActionText }}</span> the server?
|
<span class="lowercase">{{ pendingAction }}</span> the server?
|
||||||
</p>
|
</p>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="dontAskAgain"
|
v-model="dontAskAgain"
|
||||||
label="Don't ask me again"
|
label="Don't ask me again"
|
||||||
class="text-sm"
|
class="text-sm"
|
||||||
:disabled="!powerAction"
|
:disabled="!pendingAction"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row gap-4">
|
<div class="flex flex-row gap-4">
|
||||||
<ButtonStyled type="standard" color="brand" @click="executePowerAction">
|
<ButtonStyled type="standard" color="brand" @click="executePowerAction">
|
||||||
<button>
|
<button>
|
||||||
<CheckIcon class="h-5 w-5" />
|
<CheckIcon class="h-5 w-5" />
|
||||||
{{ confirmActionText }} server
|
{{ pendingAction }} server
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled @click="resetPowerAction">
|
<ButtonStyled @click="resetPowerAction">
|
||||||
@@ -31,11 +31,11 @@
|
|||||||
|
|
||||||
<NewModal
|
<NewModal
|
||||||
ref="detailsModal"
|
ref="detailsModal"
|
||||||
:header="`All of ${serverName || 'Server'} info`"
|
:header="`All of ${server.name || 'Server'} info`"
|
||||||
@close="closeDetailsModal"
|
@close="detailsModal?.hide()"
|
||||||
>
|
>
|
||||||
<ServerInfoLabels
|
<ServerInfoLabels
|
||||||
:server-data="serverData"
|
:server-data="server"
|
||||||
:show-game-label="true"
|
:show-game-label="true"
|
||||||
:show-loader-label="true"
|
:show-loader-label="true"
|
||||||
:uptime-seconds="uptimeSeconds"
|
:uptime-seconds="uptimeSeconds"
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
class="mb-6 flex flex-col gap-2"
|
class="mb-6 flex flex-col gap-2"
|
||||||
/>
|
/>
|
||||||
<div v-if="flags.advancedDebugInfo" class="markdown-body">
|
<div v-if="flags.advancedDebugInfo" class="markdown-body">
|
||||||
<pre>{{ serverData }}</pre>
|
<pre>{{ server }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled type="standard" color="brand" @click="closeDetailsModal">
|
<ButtonStyled type="standard" color="brand" @click="detailsModal?.hide()">
|
||||||
<button class="w-full">Close</button>
|
<button class="w-full">Close</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
@@ -62,14 +62,14 @@
|
|||||||
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<StopCircleIcon class="h-5 w-5" />
|
<StopCircleIcon class="h-5 w-5" />
|
||||||
<span>{{ isStoppingState ? 'Stopping...' : 'Stop' }}</span>
|
<span>{{ isStopping ? 'Stopping...' : 'Stop' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<ButtonStyled type="standard" color="brand" size="large">
|
<ButtonStyled type="standard" color="brand" size="large">
|
||||||
<button v-tooltip="busyReason" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
<div v-if="isTransitionState" class="grid place-content-center">
|
<div v-if="isTransitioning" class="grid place-content-center">
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
</div>
|
</div>
|
||||||
<component :is="isRunning ? UpdatedIcon : PlayIcon" v-else />
|
<component :is="isRunning ? UpdatedIcon : PlayIcon" v-else />
|
||||||
@@ -116,8 +116,16 @@ import {
|
|||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { ButtonStyled, Checkbox, NewModal, ServerInfoLabels } from '@modrinth/ui'
|
import {
|
||||||
import type { PowerAction as ServerPowerAction, ServerState } from '@modrinth/utils'
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
injectModrinthClient,
|
||||||
|
injectModrinthServerContext,
|
||||||
|
injectNotificationManager,
|
||||||
|
NewModal,
|
||||||
|
ServerInfoLabels,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -126,70 +134,60 @@ import LoadingIcon from './icons/LoadingIcon.vue'
|
|||||||
import PanelSpinner from './PanelSpinner.vue'
|
import PanelSpinner from './PanelSpinner.vue'
|
||||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||||
|
|
||||||
const flags = useFeatureFlags()
|
type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
|
||||||
|
|
||||||
interface PowerAction {
|
|
||||||
action: ServerPowerAction
|
|
||||||
nextState: ServerState
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOnline: boolean
|
disabled?: boolean
|
||||||
isActioning: boolean
|
|
||||||
isInstalling: boolean
|
|
||||||
disabled: boolean
|
|
||||||
serverName?: string
|
|
||||||
serverData: object
|
|
||||||
uptimeSeconds: number
|
uptimeSeconds: number
|
||||||
busyReason?: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'action', action: ServerPowerAction): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const flags = useFeatureFlags()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const serverId = router.currentRoute.value.params.id
|
const client = injectModrinthClient()
|
||||||
|
const { serverId, server, powerState, busyReasons } = injectModrinthServerContext()
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null)
|
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null)
|
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
|
const pendingAction = ref<PowerAction | null>(null)
|
||||||
|
const dontAskAgain = ref(false)
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||||
powerDontAskAgain: false,
|
powerDontAskAgain: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const serverState = ref<ServerState>(props.isOnline ? 'running' : 'stopped')
|
const isInstalling = computed(() => server.value.status === 'installing')
|
||||||
const powerAction = ref<PowerAction | null>(null)
|
const isRunning = computed(() => powerState.value === 'running')
|
||||||
const dontAskAgain = ref(false)
|
const isStopping = computed(() => powerState.value === 'stopping')
|
||||||
const startingDelay = ref(false)
|
const isTransitioning = computed(
|
||||||
|
() => powerState.value === 'starting' || powerState.value === 'stopping',
|
||||||
|
)
|
||||||
|
const showStopButton = computed(() => isRunning.value || isStopping.value)
|
||||||
|
|
||||||
|
const busyTooltip = computed(() =>
|
||||||
|
busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined,
|
||||||
|
)
|
||||||
|
|
||||||
const canTakeAction = computed(
|
const canTakeAction = computed(
|
||||||
() => !props.isActioning && !startingDelay.value && !isTransitionState.value && !props.busyReason,
|
() => !isTransitioning.value && !props.disabled && busyReasons.value.length === 0,
|
||||||
)
|
)
|
||||||
const isRunning = computed(() => serverState.value === 'running')
|
|
||||||
const isTransitionState = computed(() =>
|
|
||||||
['starting', 'stopping', 'restarting'].includes(serverState.value),
|
|
||||||
)
|
|
||||||
const isStoppingState = computed(() => serverState.value === 'stopping')
|
|
||||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value)
|
|
||||||
|
|
||||||
const primaryActionText = computed(() => {
|
const primaryActionText = computed(() => {
|
||||||
const states: Partial<Record<ServerState, string>> = {
|
switch (powerState.value) {
|
||||||
starting: 'Starting...',
|
case 'starting':
|
||||||
restarting: 'Restarting...',
|
return 'Starting...'
|
||||||
running: 'Restart',
|
case 'stopping':
|
||||||
stopping: 'Stopping...',
|
return 'Stopping...'
|
||||||
stopped: 'Start',
|
case 'running':
|
||||||
|
return 'Restart'
|
||||||
|
default:
|
||||||
|
return 'Start'
|
||||||
}
|
}
|
||||||
return states[serverState.value]
|
|
||||||
})
|
|
||||||
|
|
||||||
const confirmActionText = computed(() => {
|
|
||||||
if (!powerAction.value) return ''
|
|
||||||
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const menuOptions = computed(() => [
|
const menuOptions = computed(() => [
|
||||||
...(props.isInstalling
|
...(isInstalling.value
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
@@ -221,28 +219,31 @@ const menuOptions = computed(() => [
|
|||||||
])
|
])
|
||||||
|
|
||||||
async function copyId() {
|
async function copyId() {
|
||||||
await navigator.clipboard.writeText(serverId as string)
|
await navigator.clipboard.writeText(serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initiateAction(action: ServerPowerAction) {
|
async function sendPowerAction(action: PowerAction) {
|
||||||
|
try {
|
||||||
|
await client.archon.servers_v0.power(serverId, action)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error performing ${action} on server:`, error)
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: `Failed to ${action.toLowerCase()} server`,
|
||||||
|
text: 'An error occurred while performing this action.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initiateAction(action: PowerAction) {
|
||||||
if (!canTakeAction.value) return
|
if (!canTakeAction.value) return
|
||||||
|
|
||||||
const stateMap: Record<ServerPowerAction, ServerState> = {
|
|
||||||
Start: 'starting',
|
|
||||||
Stop: 'stopping',
|
|
||||||
Restart: 'restarting',
|
|
||||||
Kill: 'stopping',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === 'Start') {
|
if (action === 'Start') {
|
||||||
emit('action', action)
|
sendPowerAction(action)
|
||||||
serverState.value = stateMap[action]
|
|
||||||
startingDelay.value = true
|
|
||||||
setTimeout(() => (startingDelay.value = false), 5000)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
powerAction.value = { action, nextState: stateMap[action] }
|
pendingAction.value = action
|
||||||
|
|
||||||
if (userPreferences.value.powerDontAskAgain) {
|
if (userPreferences.value.powerDontAskAgain) {
|
||||||
executePowerAction()
|
executePowerAction()
|
||||||
@@ -256,41 +257,20 @@ function handlePrimaryAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function executePowerAction() {
|
function executePowerAction() {
|
||||||
if (!powerAction.value) return
|
if (!pendingAction.value) return
|
||||||
|
|
||||||
const { action, nextState } = powerAction.value
|
sendPowerAction(pendingAction.value)
|
||||||
emit('action', action)
|
|
||||||
serverState.value = nextState
|
|
||||||
|
|
||||||
if (dontAskAgain.value) {
|
if (dontAskAgain.value) {
|
||||||
userPreferences.value.powerDontAskAgain = true
|
userPreferences.value.powerDontAskAgain = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'Start') {
|
|
||||||
startingDelay.value = true
|
|
||||||
setTimeout(() => (startingDelay.value = false), 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPowerAction()
|
resetPowerAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPowerAction() {
|
function resetPowerAction() {
|
||||||
confirmActionModal.value?.hide()
|
confirmActionModal.value?.hide()
|
||||||
powerAction.value = null
|
pendingAction.value = null
|
||||||
dontAskAgain.value = false
|
dontAskAgain.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetailsModal() {
|
|
||||||
detailsModal.value?.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.isOnline,
|
|
||||||
(online) => (serverState.value = online ? 'running' : 'stopped'),
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => router.currentRoute.value.fullPath,
|
|
||||||
() => closeDetailsModal(),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="props.restart" type="standard" color="brand">
|
<ButtonStyled v-if="props.restart" type="standard" color="brand">
|
||||||
<button :disabled="props.isUpdating" @click="saveAndRestart">
|
<button :disabled="props.isUpdating || isTransitioning" @click="saveAndPower">
|
||||||
{{ props.isUpdating ? 'Saving...' : 'Save & restart' }}
|
{{ powerButtonLabel }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, injectModrinthClient } from '@modrinth/ui'
|
import { ButtonStyled, injectModrinthClient, injectModrinthServerContext } from '@modrinth/ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isUpdating: boolean
|
isUpdating: boolean
|
||||||
@@ -42,10 +43,23 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
|
const { powerState } = injectModrinthServerContext()
|
||||||
|
|
||||||
const saveAndRestart = async () => {
|
const isStopped = computed(() => powerState.value === 'stopped' || powerState.value === 'crashed')
|
||||||
|
|
||||||
|
const isTransitioning = computed(
|
||||||
|
() => powerState.value === 'starting' || powerState.value === 'stopping',
|
||||||
|
)
|
||||||
|
|
||||||
|
const powerButtonLabel = computed(() => {
|
||||||
|
if (props.isUpdating) return 'Saving...'
|
||||||
|
if (isTransitioning.value) return isStopped.value ? 'Save & start' : 'Save & restart'
|
||||||
|
return isStopped.value ? 'Save & start' : 'Save & restart'
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveAndPower = async () => {
|
||||||
props.save()
|
props.save()
|
||||||
await client.archon.servers_v0.power(props.serverId, 'Restart')
|
await client.archon.servers_v0.power(props.serverId, isStopped.value ? 'Start' : 'Restart')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -172,17 +172,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div v-if="isConnected && !serverData.flows?.intro" class="flex gap-2">
|
<div v-if="isConnected && !serverData.flows?.intro" class="flex gap-2">
|
||||||
<PanelServerActionButton
|
<PanelServerActionButton :disabled="!!error" :uptime-seconds="uptimeSeconds" />
|
||||||
:is-online="isServerRunning"
|
|
||||||
:is-actioning="isActioning"
|
|
||||||
:is-installing="serverData.status === 'installing'"
|
|
||||||
:disabled="isActioning || !!error"
|
|
||||||
:server-name="serverData.name"
|
|
||||||
:server-data="serverData"
|
|
||||||
:uptime-seconds="uptimeSeconds"
|
|
||||||
:busy-reason="busyReasons.length > 0 ? formatMessage(busyReasons[0].reason) : undefined"
|
|
||||||
@action="sendPowerAction"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ContentPageHeader>
|
</ContentPageHeader>
|
||||||
@@ -320,12 +310,14 @@
|
|||||||
>
|
>
|
||||||
<InstallingBanner
|
<InstallingBanner
|
||||||
v-if="
|
v-if="
|
||||||
(serverData.status === 'installing' || isSyncingContent) &&
|
(serverData.status === 'installing' || isSyncingContent || contentError) &&
|
||||||
syncProgress?.phase !== 'Analyzing'
|
syncProgress?.phase !== 'Analyzing'
|
||||||
"
|
"
|
||||||
data-pyro-server-installing
|
data-pyro-server-installing
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:progress="syncProgress"
|
:progress="syncProgress"
|
||||||
|
:content-error="contentError"
|
||||||
|
@retry="handleContentRetry"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ServerIcon :image="serverImage" class="!h-6 !w-6" />
|
<ServerIcon :image="serverImage" class="!h-6 !w-6" />
|
||||||
@@ -398,9 +390,8 @@ import {
|
|||||||
ServerNotice,
|
ServerNotice,
|
||||||
ServerOnboardingPanelPage,
|
ServerOnboardingPanelPage,
|
||||||
useDebugLogger,
|
useDebugLogger,
|
||||||
useVIntl,
|
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { PowerAction, Stats } from '@modrinth/utils'
|
import type { Stats } from '@modrinth/utils'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useTimeoutFn } from '@vueuse/core'
|
import { useTimeoutFn } from '@vueuse/core'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
@@ -416,7 +407,6 @@ import { useServerProject } from '~/composables/servers/use-server-project.ts'
|
|||||||
import { useModrinthServersConsole } from '~/store/console.ts'
|
import { useModrinthServersConsole } from '~/store/console.ts'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
|
|
||||||
const isReconnecting = ref(false)
|
const isReconnecting = ref(false)
|
||||||
@@ -505,7 +495,6 @@ const modrinthServersConsole = useModrinthServersConsole()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const cpuData = ref<number[]>([])
|
const cpuData = ref<number[]>([])
|
||||||
const ramData = ref<number[]>([])
|
const ramData = ref<number[]>([])
|
||||||
const isActioning = ref(false)
|
|
||||||
const isServerRunning = computed(() => serverPowerState.value === 'running')
|
const isServerRunning = computed(() => serverPowerState.value === 'running')
|
||||||
const serverPowerState = ref<Archon.Websocket.v0.PowerState>('stopped')
|
const serverPowerState = ref<Archon.Websocket.v0.PowerState>('stopped')
|
||||||
const powerStateDetails = ref<{ oom_killed?: boolean; exit_code?: number }>()
|
const powerStateDetails = ref<{ oom_killed?: boolean; exit_code?: number }>()
|
||||||
@@ -518,6 +507,7 @@ const markBackupCancelled = (backupId: string) => {
|
|||||||
|
|
||||||
// Parthenon state event
|
// Parthenon state event
|
||||||
const syncProgress = ref<Archon.Websocket.v0.SyncContentProgress | null>(null)
|
const syncProgress = ref<Archon.Websocket.v0.SyncContentProgress | null>(null)
|
||||||
|
const contentError = ref<Archon.Websocket.v0.SyncContentError | null>(null)
|
||||||
const syncProgressActive = ref(false)
|
const syncProgressActive = ref(false)
|
||||||
const isAwaitingPostInstallRefresh = ref(false)
|
const isAwaitingPostInstallRefresh = ref(false)
|
||||||
const { start: startSyncHide, stop: cancelSyncHide } = useTimeoutFn(
|
const { start: startSyncHide, stop: cancelSyncHide } = useTimeoutFn(
|
||||||
@@ -845,6 +835,7 @@ const handleState = (data: Archon.Websocket.v0.WSStateEvent) => {
|
|||||||
serverStatus: serverData.value?.status,
|
serverStatus: serverData.value?.status,
|
||||||
})
|
})
|
||||||
syncProgress.value = data.progress
|
syncProgress.value = data.progress
|
||||||
|
contentError.value = data.content_error
|
||||||
|
|
||||||
// Sync power state from the state event
|
// Sync power state from the state event
|
||||||
const powerMap: Record<Archon.Websocket.v0.FlattenedPowerState, Archon.Websocket.v0.PowerState> =
|
const powerMap: Record<Archon.Websocket.v0.FlattenedPowerState, Archon.Websocket.v0.PowerState> =
|
||||||
@@ -878,6 +869,7 @@ const handleState = (data: Archon.Websocket.v0.WSStateEvent) => {
|
|||||||
hasSeenInstallProgress = true
|
hasSeenInstallProgress = true
|
||||||
} else if (
|
} else if (
|
||||||
data.progress == null &&
|
data.progress == null &&
|
||||||
|
data.content_error == null &&
|
||||||
serverData.value.status === 'installing' &&
|
serverData.value.status === 'installing' &&
|
||||||
hasSeenInstallProgress
|
hasSeenInstallProgress
|
||||||
) {
|
) {
|
||||||
@@ -889,6 +881,18 @@ const handleState = (data: Archon.Websocket.v0.WSStateEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleContentRetry() {
|
||||||
|
if (!worldId.value) return
|
||||||
|
try {
|
||||||
|
await client.archon.content_v1.repair(serverId, worldId.value)
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
text: err instanceof Error ? err.message : 'Failed to retry installation',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleUptime = (data: Archon.Websocket.v0.WSUptimeEvent) => {
|
const handleUptime = (data: Archon.Websocket.v0.WSUptimeEvent) => {
|
||||||
stopUptimeUpdates()
|
stopUptimeUpdates()
|
||||||
uptimeSeconds.value = data.uptime
|
uptimeSeconds.value = data.uptime
|
||||||
@@ -1219,43 +1223,6 @@ const updateGraphData = (dataArray: number[], newValue: number): number[] => {
|
|||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
const toAdverb = (word: string) => {
|
|
||||||
if (word.endsWith('p')) {
|
|
||||||
return word + 'ping'
|
|
||||||
}
|
|
||||||
if (word.endsWith('e')) {
|
|
||||||
return word.slice(0, -1) + 'ing'
|
|
||||||
}
|
|
||||||
if (word.endsWith('ie')) {
|
|
||||||
return word.slice(0, -2) + 'ying'
|
|
||||||
}
|
|
||||||
return word + 'ing'
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendPowerAction = async (action: PowerAction) => {
|
|
||||||
const actionName = action.charAt(0).toUpperCase() + action.slice(1)
|
|
||||||
try {
|
|
||||||
isActioning.value = true
|
|
||||||
await client.archon.servers_v0.power(serverId, action)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error ${toAdverb(actionName)} server:`, error)
|
|
||||||
notifyError(
|
|
||||||
`Error ${toAdverb(actionName)} server`,
|
|
||||||
'An error occurred while performing this action.',
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
isActioning.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyError = (title: string, text: string) => {
|
|
||||||
addNotification({
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeUnavailableDetails = computed(() => [
|
const nodeUnavailableDetails = computed(() => [
|
||||||
{
|
{
|
||||||
label: 'Server ID',
|
label: 'Server ID',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@proceed="confirmResetToOnboarding"
|
@proceed="confirmResetToOnboarding"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InstallationSettingsLayout ref="installationSettingsLayout">
|
<InstallationSettingsLayout ref="installationSettingsLayout" @reset-server="setupModal?.show()">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex flex-col gap-2.5">
|
<div class="flex flex-col gap-2.5">
|
||||||
<span class="text-lg font-semibold text-contrast">{{
|
<span class="text-lg font-semibold text-contrast">{{
|
||||||
@@ -455,6 +455,9 @@ provideInstallationSettings({
|
|||||||
debug('save: called with', { platform, gameVersion, loaderVersionId })
|
debug('save: called with', { platform, gameVersion, loaderVersionId })
|
||||||
const currentPlatform = server.value?.loader?.toLowerCase() ?? 'vanilla'
|
const currentPlatform = server.value?.loader?.toLowerCase() ?? 'vanilla'
|
||||||
const platformChanged = platform !== currentPlatform
|
const platformChanged = platform !== currentPlatform
|
||||||
|
const gameVersionChanged = gameVersion !== (server.value?.mc_version ?? '')
|
||||||
|
const loaderVersionChanged =
|
||||||
|
loaderVersionId !== null && loaderVersionId !== (server.value?.loader_version ?? '')
|
||||||
|
|
||||||
let resolvedLoaderVersion = loaderVersionId
|
let resolvedLoaderVersion = loaderVersionId
|
||||||
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
||||||
@@ -465,12 +468,12 @@ provideInstallationSettings({
|
|||||||
debug('save: emitting reinstall before API call')
|
debug('save: emitting reinstall before API call')
|
||||||
emit(
|
emit(
|
||||||
'reinstall',
|
'reinstall',
|
||||||
platformChanged
|
platformChanged || loaderVersionChanged
|
||||||
? { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion }
|
? { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion }
|
||||||
: { mVersion: gameVersion },
|
: { mVersion: gameVersion },
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
if (platformChanged) {
|
if (platformChanged || loaderVersionChanged) {
|
||||||
const request: Archon.Content.v1.InstallWorldContent = {
|
const request: Archon.Content.v1.InstallWorldContent = {
|
||||||
content_variant: 'bare',
|
content_variant: 'bare',
|
||||||
loader: toApiLoader(platform),
|
loader: toApiLoader(platform),
|
||||||
@@ -478,9 +481,9 @@ provideInstallationSettings({
|
|||||||
game_version: gameVersion || undefined,
|
game_version: gameVersion || undefined,
|
||||||
soft_override: true,
|
soft_override: true,
|
||||||
}
|
}
|
||||||
debug('save: platform changed, calling installContent', request)
|
debug('save: platform/loader version changed, calling installContent', request)
|
||||||
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
||||||
} else {
|
} else if (gameVersionChanged) {
|
||||||
debug('save: game version only, calling applyGameVersionUpdate', gameVersion)
|
debug('save: game version only, calling applyGameVersionUpdate', gameVersion)
|
||||||
await client.archon.content_v1.applyGameVersionUpdate(serverId, worldId.value!, gameVersion)
|
await client.archon.content_v1.applyGameVersionUpdate(serverId, worldId.value!, gameVersion)
|
||||||
}
|
}
|
||||||
@@ -662,8 +665,66 @@ provideInstallationSettings({
|
|||||||
isApp: false,
|
isApp: false,
|
||||||
showModpackVersionActions: computed(() => modpack.value?.spec.platform === 'modrinth'),
|
showModpackVersionActions: computed(() => modpack.value?.spec.platform === 'modrinth'),
|
||||||
|
|
||||||
lockPlatform: true,
|
lockPlatform: false,
|
||||||
hideLoaderVersion: true,
|
hideLoaderVersion: false,
|
||||||
|
|
||||||
|
async disableAllContent() {
|
||||||
|
debug('disableAllContent: fetching all addons')
|
||||||
|
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
||||||
|
const items = (addons.addons ?? [])
|
||||||
|
.filter((a) => !a.disabled)
|
||||||
|
.map((a) => ({ kind: a.kind, filename: a.filename }))
|
||||||
|
if (items.length > 0) {
|
||||||
|
debug('disableAllContent: disabling', items.length, 'addons')
|
||||||
|
await client.archon.content_v1.disableAddons(serverId, worldId.value!, items)
|
||||||
|
}
|
||||||
|
debug('disableAllContent: done')
|
||||||
|
},
|
||||||
|
|
||||||
|
async disableIncompatibleContent(diffs) {
|
||||||
|
debug('disableIncompatibleContent: processing', diffs.length, 'diffs')
|
||||||
|
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
||||||
|
const removedFiles = new Set(diffs.filter((d) => d.type === 'removed').map((d) => d.fileName))
|
||||||
|
const items = (addons.addons ?? [])
|
||||||
|
.filter((a) => !a.disabled && removedFiles.has(a.filename))
|
||||||
|
.map((a) => ({ kind: a.kind, filename: a.filename }))
|
||||||
|
if (items.length > 0) {
|
||||||
|
debug('disableIncompatibleContent: disabling', items.length, 'addons')
|
||||||
|
await client.archon.content_v1.disableAddons(serverId, worldId.value!, items)
|
||||||
|
}
|
||||||
|
debug('disableIncompatibleContent: done')
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveWithoutAutoFix(platform, gameVersion, loaderVersionId) {
|
||||||
|
debug('saveWithoutAutoFix: called with', { platform, gameVersion, loaderVersionId })
|
||||||
|
let resolvedLoaderVersion = loaderVersionId
|
||||||
|
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
||||||
|
const versions = getLoaderVersionsForGameVersion(platform, gameVersion)
|
||||||
|
resolvedLoaderVersion = versions[0]?.id ?? null
|
||||||
|
}
|
||||||
|
emit('reinstall', { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion })
|
||||||
|
try {
|
||||||
|
const request: Archon.Content.v1.InstallWorldContent = {
|
||||||
|
content_variant: 'bare',
|
||||||
|
loader: toApiLoader(platform),
|
||||||
|
version: resolvedLoaderVersion ?? '',
|
||||||
|
game_version: gameVersion || undefined,
|
||||||
|
soft_override: true,
|
||||||
|
}
|
||||||
|
debug('saveWithoutAutoFix: calling installContent', request)
|
||||||
|
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
||||||
|
debug('saveWithoutAutoFix: succeeded, invalidating')
|
||||||
|
invalidateServerState()
|
||||||
|
} catch (err) {
|
||||||
|
debug('saveWithoutAutoFix: failed', err)
|
||||||
|
emit('reinstall-failed')
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
text: err instanceof Error ? err.message : formatMessage(messages.failedToSaveSettings),
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async previewSave(_platform, gameVersion, _loaderVersionId, signal) {
|
async previewSave(_platform, gameVersion, _loaderVersionId, signal) {
|
||||||
const result = await client.archon.content_v1.getUpdateGameVersionPreview(
|
const result = await client.archon.content_v1.getUpdateGameVersionPreview(
|
||||||
|
|||||||
@@ -611,6 +611,11 @@ export namespace Archon {
|
|||||||
percent: number
|
percent: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SyncContentError = {
|
||||||
|
step: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
export type WSStateEvent = {
|
export type WSStateEvent = {
|
||||||
event: 'state'
|
event: 'state'
|
||||||
debug: string
|
debug: string
|
||||||
@@ -620,6 +625,7 @@ export namespace Archon {
|
|||||||
target: 'start' | 'stop' | 'restart' | null
|
target: 'start' | 'stop' | 'restart' | null
|
||||||
uptime: number
|
uptime: number
|
||||||
progress: SyncContentProgress | null
|
progress: SyncContentProgress | null
|
||||||
|
content_error: SyncContentError | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outgoing messages (client -> server)
|
// Outgoing messages (client -> server)
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ watch(
|
|||||||
// Always fetch the actual latest version from the API since search index can be stale
|
// Always fetch the actual latest version from the API since search index can be stale
|
||||||
try {
|
try {
|
||||||
const versions = await ctx.getProjectVersions(projectId)
|
const versions = await ctx.getProjectVersions(projectId)
|
||||||
|
if (ctx.modpackSearchProjectId.value !== projectId) return
|
||||||
if (versions.length > 0) {
|
if (versions.length > 0) {
|
||||||
const version = versions[0]
|
const version = versions[0]
|
||||||
ctx.modpackSelection.value = {
|
ctx.modpackSelection.value = {
|
||||||
|
|||||||
@@ -307,8 +307,19 @@ export function createCreationFlowContext(
|
|||||||
isImportMode.value = false
|
isImportMode.value = false
|
||||||
setupType.value = type
|
setupType.value = type
|
||||||
if (type === 'modpack') {
|
if (type === 'modpack') {
|
||||||
|
selectedLoader.value = null
|
||||||
|
selectedLoaderVersion.value = null
|
||||||
|
loaderVersionType.value = 'stable'
|
||||||
modal.value?.setStage('modpack')
|
modal.value?.setStage('modpack')
|
||||||
} else {
|
} else {
|
||||||
|
modpackSelection.value = null
|
||||||
|
modpackFile.value = null
|
||||||
|
modpackFilePath.value = null
|
||||||
|
if (type === 'vanilla') {
|
||||||
|
selectedLoader.value = null
|
||||||
|
selectedLoaderVersion.value = null
|
||||||
|
loaderVersionType.value = 'stable'
|
||||||
|
}
|
||||||
// both custom and vanilla go to custom-setup
|
// both custom and vanilla go to custom-setup
|
||||||
// vanilla just hides loader chips via hideLoaderChips computed
|
// vanilla just hides loader chips via hideLoaderChips computed
|
||||||
modal.value?.setStage('custom-setup')
|
modal.value?.setStage('custom-setup')
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<Admonition type="info" show-actions-underneath>
|
<Admonition :type="contentError ? 'critical' : 'info'" :show-actions-underneath="!contentError">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<slot name="icon">
|
<slot v-if="!contentError" name="icon">
|
||||||
<SpinnerIcon class="h-6 w-6 flex-none animate-spin text-brand-blue" />
|
<SpinnerIcon class="h-6 w-6 flex-none animate-spin text-brand-blue" />
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template #header>We're preparing your server!</template>
|
<template #header>
|
||||||
<template v-if="progress">{{ phaseLabel }}</template>
|
{{ contentError ? 'Installation error' : "We're preparing your server!" }}
|
||||||
|
</template>
|
||||||
|
<template v-if="contentError">
|
||||||
|
{{ errorLabel }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="progress">{{ phaseLabel }}</template>
|
||||||
<div v-else class="ticker-container">
|
<div v-else class="ticker-container">
|
||||||
<div class="ticker-content">
|
<div class="ticker-content">
|
||||||
<div
|
<div
|
||||||
@@ -19,7 +24,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #actions>
|
<template v-if="contentError" #top-right-actions>
|
||||||
|
<ButtonStyled color="red" type="outlined">
|
||||||
|
<button class="!border" @click="emit('retry')">
|
||||||
|
<RotateCounterClockwiseIcon class="size-5" />
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
<template v-if="!contentError" #actions>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
v-if="progress"
|
v-if="progress"
|
||||||
:progress="progress.percent"
|
:progress="progress.percent"
|
||||||
@@ -33,10 +46,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { RotateCounterClockwiseIcon } from '@modrinth/assets'
|
||||||
import SpinnerIcon from '@modrinth/assets/icons/spinner.svg'
|
import SpinnerIcon from '@modrinth/assets/icons/spinner.svg'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
import Admonition from '../base/Admonition.vue'
|
import Admonition from '../base/Admonition.vue'
|
||||||
|
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||||
import ProgressBar from '../base/ProgressBar.vue'
|
import ProgressBar from '../base/ProgressBar.vue'
|
||||||
|
|
||||||
export interface SyncProgress {
|
export interface SyncProgress {
|
||||||
@@ -44,10 +59,48 @@ export interface SyncProgress {
|
|||||||
percent: number
|
percent: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContentError {
|
||||||
|
step: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
progress?: SyncProgress | null
|
progress?: SyncProgress | null
|
||||||
|
contentError?: ContentError | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
retry: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const errorLabel = computed(() => {
|
||||||
|
const desc = props.contentError?.description?.toLowerCase()
|
||||||
|
const step = props.contentError?.step
|
||||||
|
|
||||||
|
if (step === 'modloader') {
|
||||||
|
if (desc === 'the specified version may be incorrect') {
|
||||||
|
return 'The specified loader or Minecraft version could not be installed. It may be invalid or unsupported.'
|
||||||
|
}
|
||||||
|
if (desc === 'this version is not yet supported') {
|
||||||
|
return 'This version of Minecraft or loader is not yet supported by Modrinth Hosting.'
|
||||||
|
}
|
||||||
|
if (desc === 'internal error') {
|
||||||
|
return 'An internal error occurred while installing the platform. Please try again.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 'modpack') {
|
||||||
|
if (desc?.includes('no primary file')) {
|
||||||
|
return 'The modpack version has no downloadable file. It may have been packaged incorrectly.'
|
||||||
|
}
|
||||||
|
if (desc?.includes('failed to install')) {
|
||||||
|
return 'Failed to install the modpack. It may be corrupted or incompatible.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.contentError?.description ?? 'An unexpected error occurred during installation.'
|
||||||
|
})
|
||||||
|
|
||||||
const phaseLabel = computed(() => {
|
const phaseLabel = computed(() => {
|
||||||
switch (props.progress?.phase) {
|
switch (props.progress?.phase) {
|
||||||
case 'InstallingLoader':
|
case 'InstallingLoader':
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ onUnmounted(() => {
|
|||||||
class="@container flex flex-col gap-4 rounded-[20px] bg-bg-raised p-6 shadow-md"
|
class="@container flex flex-col gap-4 rounded-[20px] bg-bg-raised p-6 shadow-md"
|
||||||
:class="{ 'opacity-50': disabled }"
|
:class="{ 'opacity-50': disabled }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-4">
|
<div class="flex min-w-0 flex-1 items-center gap-4">
|
||||||
<AutoLink :to="projectLink" class="shrink-0">
|
<AutoLink :to="projectLink" class="shrink-0">
|
||||||
<Avatar :src="project.icon_url" :alt="project.title" size="5rem" no-shadow raised />
|
<Avatar :src="project.icon_url" :alt="project.title" size="5rem" no-shadow raised />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<InlineBackupCreator
|
<InlineBackupCreator
|
||||||
ref="backupCreator"
|
ref="backupCreator"
|
||||||
:backup-name="backupTip ? `Before bulk update (${backupTip})` : 'Before bulk update'"
|
:backup-name="backupTip ? `Before bulk update (${backupTip})` : 'Before bulk update'"
|
||||||
|
:shift-click-hint-override="formatMessage(messages.shiftClickHint)"
|
||||||
@update:buttons-disabled="buttonsDisabled = $event"
|
@update:buttons-disabled="buttonsDisabled = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +69,11 @@ const messages = defineMessages({
|
|||||||
id: 'content.confirm-bulk-update.update-button',
|
id: 'content.confirm-bulk-update.update-button',
|
||||||
defaultMessage: 'Update {count, plural, one {# project} other {# projects}}',
|
defaultMessage: 'Update {count, plural, one {# project} other {# projects}}',
|
||||||
},
|
},
|
||||||
|
shiftClickHint: {
|
||||||
|
id: 'content.confirm-bulk-update.shift-click-hint',
|
||||||
|
defaultMessage:
|
||||||
|
'Hold Shift while clicking "Update all" to skip this confirmation in the future.',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="!props.hideShiftClickHint" class="text-secondary">
|
<span v-if="!props.hideShiftClickHint" class="text-secondary">
|
||||||
{{ formatMessage(messages.shiftClickHint) }}
|
{{ props.shiftClickHintOverride ?? formatMessage(messages.shiftClickHint) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,6 +61,7 @@ import { useInlineBackup } from '../../composables/use-inline-backup'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
backupName: string
|
backupName: string
|
||||||
hideShiftClickHint?: boolean
|
hideShiftClickHint?: boolean
|
||||||
|
shiftClickHintOverride?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
<InlineBackupCreator
|
<InlineBackupCreator
|
||||||
ref="backupCreator"
|
ref="backupCreator"
|
||||||
backup-name="Before version change"
|
backup-name="Before version change"
|
||||||
|
hide-shift-click-hint
|
||||||
@update:buttons-disabled="buttonsDisabled = $event"
|
@update:buttons-disabled="buttonsDisabled = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal ref="modal" :header="formatMessage(messages.header)" :closable="!loading" no-padding>
|
||||||
|
<div class="flex max-w-[500px] flex-col gap-6 p-6">
|
||||||
|
<Admonition
|
||||||
|
:type="variant === 'loader-change' ? 'critical' : 'warning'"
|
||||||
|
:header="
|
||||||
|
variant === 'loader-change'
|
||||||
|
? formatMessage(messages.loaderChangeTitle)
|
||||||
|
: formatMessage(messages.gameVersionWarningTitle)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
variant === 'loader-change'
|
||||||
|
? formatMessage(messages.loaderChangeBody)
|
||||||
|
: formatMessage(messages.gameVersionWarningBody)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<div v-if="variant === 'loader-change'">
|
||||||
|
<ButtonStyled color="red">
|
||||||
|
<button :disabled="loading" @click="handleResetServer">
|
||||||
|
<TrashIcon class="size-5" />
|
||||||
|
{{ formatMessage(commonMessages.resetServerButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<InlineBackupCreator
|
||||||
|
ref="backupCreator"
|
||||||
|
:backup-name="
|
||||||
|
variant === 'loader-change' ? 'Before loader change' : 'Before version change'
|
||||||
|
"
|
||||||
|
hide-shift-click-hint
|
||||||
|
@update:buttons-disabled="buttonsDisabled = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button :disabled="loading" @click="handleCancel">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<template v-if="variant === 'game-version-change'">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button :disabled="buttonsDisabled || loading" @click="handleDisableConflicts">
|
||||||
|
<SpinnerIcon
|
||||||
|
v-if="loading && loadingAction === 'disable-conflicts'"
|
||||||
|
class="size-5 animate-spin"
|
||||||
|
/>
|
||||||
|
<PowerOffIcon v-else class="size-5" />
|
||||||
|
{{ formatMessage(messages.disableConflictsButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="orange">
|
||||||
|
<button :disabled="buttonsDisabled || loading" @click="handleAutoFix">
|
||||||
|
<SpinnerIcon
|
||||||
|
v-if="loading && loadingAction === 'auto-fix'"
|
||||||
|
class="size-5 animate-spin"
|
||||||
|
/>
|
||||||
|
<HammerIcon v-else class="size-5" />
|
||||||
|
{{ formatMessage(messages.autoFixButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ButtonStyled color="red">
|
||||||
|
<button :disabled="buttonsDisabled || loading" @click="handleConfirmLoaderChange">
|
||||||
|
<SpinnerIcon v-if="loading" class="size-5 animate-spin" />
|
||||||
|
<CircleAlertIcon v-else class="size-5" />
|
||||||
|
{{ formatMessage(messages.changeLoaderButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
CircleAlertIcon,
|
||||||
|
HammerIcon,
|
||||||
|
PowerOffIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
TrashIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
|
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||||
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
|
|
||||||
|
import InlineBackupCreator from '../../content-tab/components/modals/InlineBackupCreator.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
variant: 'loader-change' | 'game-version-change'
|
||||||
|
loading?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'confirm-loader-change': []
|
||||||
|
'auto-fix': []
|
||||||
|
'disable-conflicts': []
|
||||||
|
'reset-server': []
|
||||||
|
cancel: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const buttonsDisabled = ref(false)
|
||||||
|
const loadingAction = ref<'auto-fix' | 'disable-conflicts' | null>(null)
|
||||||
|
|
||||||
|
function show(e?: MouseEvent) {
|
||||||
|
loadingAction.value = null
|
||||||
|
modal.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
hide()
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirmLoaderChange() {
|
||||||
|
emit('confirm-loader-change')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAutoFix() {
|
||||||
|
loadingAction.value = 'auto-fix'
|
||||||
|
emit('auto-fix')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisableConflicts() {
|
||||||
|
loadingAction.value = 'disable-conflicts'
|
||||||
|
emit('disable-conflicts')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResetServer() {
|
||||||
|
hide()
|
||||||
|
emit('reset-server')
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: {
|
||||||
|
id: 'installation-settings.incompatible-content.header',
|
||||||
|
defaultMessage: 'Incompatible content installed',
|
||||||
|
},
|
||||||
|
loaderChangeTitle: {
|
||||||
|
id: 'installation-settings.incompatible-content.loader-change-title',
|
||||||
|
defaultMessage: 'Changing loaders is destructive',
|
||||||
|
},
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
gameVersionWarningTitle: {
|
||||||
|
id: 'installation-settings.incompatible-content.game-version-warning-title',
|
||||||
|
defaultMessage: 'Incompatibility warning',
|
||||||
|
},
|
||||||
|
gameVersionWarningBody: {
|
||||||
|
id: 'installation-settings.incompatible-content.game-version-warning-body',
|
||||||
|
defaultMessage:
|
||||||
|
'When changing the game version, we can either disable incompatible installed content or attempt to resolve the incompatibilities.',
|
||||||
|
},
|
||||||
|
changeLoaderButton: {
|
||||||
|
id: 'installation-settings.incompatible-content.change-loader-button',
|
||||||
|
defaultMessage: 'Change loader',
|
||||||
|
},
|
||||||
|
autoFixButton: {
|
||||||
|
id: 'installation-settings.incompatible-content.auto-fix-button',
|
||||||
|
defaultMessage: 'Auto-fix',
|
||||||
|
},
|
||||||
|
disableConflictsButton: {
|
||||||
|
id: 'installation-settings.incompatible-content.disable-conflicts-button',
|
||||||
|
defaultMessage: 'Disable conflicts',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
@@ -1 +1 @@
|
|||||||
export { useInstallationForm } from './use-installation-form'
|
export { type IncompatibleContentVariant, useInstallationForm } from './use-installation-form'
|
||||||
|
|||||||
@@ -6,13 +6,19 @@ import { formatLoaderLabel } from '#ui/utils/loaders'
|
|||||||
|
|
||||||
import type { ContentUpdaterModal } from '../../content-tab'
|
import type { ContentUpdaterModal } from '../../content-tab'
|
||||||
import type ContentDiffModal from '../components/ContentDiffModal.vue'
|
import type ContentDiffModal from '../components/ContentDiffModal.vue'
|
||||||
|
import type IncompatibleContentModal from '../components/IncompatibleContentModal.vue'
|
||||||
import type { InstallationSettingsContext } from '../providers/installation-settings'
|
import type { InstallationSettingsContext } from '../providers/installation-settings'
|
||||||
import type { ContentDiffPreview } from '../types'
|
import type { ContentDiffPreview } from '../types'
|
||||||
|
|
||||||
|
export type IncompatibleContentVariant = 'loader-change' | 'game-version-change'
|
||||||
|
|
||||||
export function useInstallationForm(
|
export function useInstallationForm(
|
||||||
ctx: InstallationSettingsContext,
|
ctx: InstallationSettingsContext,
|
||||||
updaterModalRef: Ref<InstanceType<typeof ContentUpdaterModal> | null | undefined>,
|
updaterModalRef: Ref<InstanceType<typeof ContentUpdaterModal> | null | undefined>,
|
||||||
contentDiffModalRef?: Ref<InstanceType<typeof ContentDiffModal> | null | undefined>,
|
contentDiffModalRef?: Ref<InstanceType<typeof ContentDiffModal> | null | undefined>,
|
||||||
|
incompatibleContentModalRef?: Ref<
|
||||||
|
InstanceType<typeof IncompatibleContentModal> | null | undefined
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
const selectedPlatform = ctx.editingPlatformRef ?? ref(ctx.currentPlatform.value)
|
const selectedPlatform = ctx.editingPlatformRef ?? ref(ctx.currentPlatform.value)
|
||||||
@@ -22,6 +28,7 @@ export function useInstallationForm(
|
|||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
const isVerifying = ref(false)
|
const isVerifying = ref(false)
|
||||||
const pendingPreview = ref<ContentDiffPreview | null>(null)
|
const pendingPreview = ref<ContentDiffPreview | null>(null)
|
||||||
|
const incompatibleContentVariant = ref<IncompatibleContentVariant | null>(null)
|
||||||
let abortController: AbortController | null = null
|
let abortController: AbortController | null = null
|
||||||
|
|
||||||
const gameVersionOptions = computed(() =>
|
const gameVersionOptions = computed(() =>
|
||||||
@@ -75,9 +82,26 @@ export function useInstallationForm(
|
|||||||
async function save() {
|
async function save() {
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
|
const platformChanged = selectedPlatform.value !== ctx.currentPlatform.value
|
||||||
const isModded = ctx.currentPlatform.value !== 'vanilla'
|
const isModded = ctx.currentPlatform.value !== 'vanilla'
|
||||||
const gameVersionChanged = selectedGameVersion.value !== ctx.currentGameVersion.value
|
const gameVersionChanged = selectedGameVersion.value !== ctx.currentGameVersion.value
|
||||||
|
|
||||||
|
if (platformChanged && ctx.disableAllContent) {
|
||||||
|
isSaving.value = false
|
||||||
|
incompatibleContentVariant.value = 'loader-change'
|
||||||
|
await nextTick()
|
||||||
|
incompatibleContentModalRef?.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isModded && gameVersionChanged && ctx.disableIncompatibleContent) {
|
||||||
|
isSaving.value = false
|
||||||
|
incompatibleContentVariant.value = 'game-version-change'
|
||||||
|
await nextTick()
|
||||||
|
incompatibleContentModalRef?.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.previewSave && isModded && gameVersionChanged) {
|
if (ctx.previewSave && isModded && gameVersionChanged) {
|
||||||
isVerifying.value = true
|
isVerifying.value = true
|
||||||
abortController = new AbortController()
|
abortController = new AbortController()
|
||||||
@@ -127,6 +151,111 @@ export function useInstallationForm(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmLoaderChange() {
|
||||||
|
try {
|
||||||
|
if (ctx.disableAllContent) {
|
||||||
|
await ctx.disableAllContent()
|
||||||
|
}
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
await performSave()
|
||||||
|
} catch {
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmAutoFix() {
|
||||||
|
try {
|
||||||
|
if (ctx.previewSave) {
|
||||||
|
isVerifying.value = true
|
||||||
|
abortController = new AbortController()
|
||||||
|
const loaderVersionId =
|
||||||
|
selectedPlatform.value !== 'vanilla'
|
||||||
|
? (loaderVersionEntries.value[selectedLoaderVersion.value]?.id ?? null)
|
||||||
|
: null
|
||||||
|
|
||||||
|
let preview: ContentDiffPreview | null
|
||||||
|
try {
|
||||||
|
preview = await ctx.previewSave(
|
||||||
|
selectedPlatform.value,
|
||||||
|
selectedGameVersion.value,
|
||||||
|
loaderVersionId,
|
||||||
|
abortController.signal,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
isVerifying.value = false
|
||||||
|
abortController = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview && (preview.diffs.length > 0 || preview.hasUnknownContent)) {
|
||||||
|
pendingPreview.value = preview
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
contentDiffModalRef?.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
await performSave()
|
||||||
|
} catch {
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDisableConflicts() {
|
||||||
|
try {
|
||||||
|
if (ctx.disableIncompatibleContent && ctx.previewSave) {
|
||||||
|
isVerifying.value = true
|
||||||
|
abortController = new AbortController()
|
||||||
|
const loaderVersionId =
|
||||||
|
selectedPlatform.value !== 'vanilla'
|
||||||
|
? (loaderVersionEntries.value[selectedLoaderVersion.value]?.id ?? null)
|
||||||
|
: null
|
||||||
|
|
||||||
|
let preview: ContentDiffPreview | null
|
||||||
|
try {
|
||||||
|
preview = await ctx.previewSave(
|
||||||
|
selectedPlatform.value,
|
||||||
|
selectedGameVersion.value,
|
||||||
|
loaderVersionId,
|
||||||
|
abortController.signal,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
isVerifying.value = false
|
||||||
|
abortController = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
await ctx.disableIncompatibleContent(preview.diffs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
if (ctx.saveWithoutAutoFix) {
|
||||||
|
const loaderVersionId =
|
||||||
|
selectedPlatform.value !== 'vanilla'
|
||||||
|
? (loaderVersionEntries.value[selectedLoaderVersion.value]?.id ?? null)
|
||||||
|
: null
|
||||||
|
await ctx.saveWithoutAutoFix(
|
||||||
|
selectedPlatform.value,
|
||||||
|
selectedGameVersion.value,
|
||||||
|
loaderVersionId,
|
||||||
|
)
|
||||||
|
if (ctx.afterSave) await ctx.afterSave()
|
||||||
|
isEditing.value = false
|
||||||
|
isSaving.value = false
|
||||||
|
} else {
|
||||||
|
await performSave()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmSave() {
|
async function confirmSave() {
|
||||||
pendingPreview.value = null
|
pendingPreview.value = null
|
||||||
try {
|
try {
|
||||||
@@ -138,6 +267,7 @@ export function useInstallationForm(
|
|||||||
|
|
||||||
function cancelPreview() {
|
function cancelPreview() {
|
||||||
pendingPreview.value = null
|
pendingPreview.value = null
|
||||||
|
incompatibleContentVariant.value = null
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +392,11 @@ export function useInstallationForm(
|
|||||||
hasChanges,
|
hasChanges,
|
||||||
save,
|
save,
|
||||||
pendingPreview,
|
pendingPreview,
|
||||||
|
incompatibleContentVariant,
|
||||||
confirmSave,
|
confirmSave,
|
||||||
|
confirmLoaderChange,
|
||||||
|
confirmAutoFix,
|
||||||
|
confirmDisableConflicts,
|
||||||
cancelPreview,
|
cancelPreview,
|
||||||
cancelEditing,
|
cancelEditing,
|
||||||
updatingModpack,
|
updatingModpack,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import Combobox from '#ui/components/base/Combobox.vue'
|
|||||||
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import { commonMessages } from '#ui/utils/common-messages'
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
|
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||||
|
|
||||||
import ConfirmModpackUpdateModal from '../content-tab/components/modals/ConfirmModpackUpdateModal.vue'
|
import ConfirmModpackUpdateModal from '../content-tab/components/modals/ConfirmModpackUpdateModal.vue'
|
||||||
import ConfirmReinstallModal from '../content-tab/components/modals/ConfirmReinstallModal.vue'
|
import ConfirmReinstallModal from '../content-tab/components/modals/ConfirmReinstallModal.vue'
|
||||||
@@ -31,6 +32,7 @@ import ConfirmRepairModal from '../content-tab/components/modals/ConfirmRepairMo
|
|||||||
import ConfirmUnlinkModal from '../content-tab/components/modals/ConfirmUnlinkModal.vue'
|
import ConfirmUnlinkModal from '../content-tab/components/modals/ConfirmUnlinkModal.vue'
|
||||||
import ContentUpdaterModal from '../content-tab/components/modals/ContentUpdaterModal.vue'
|
import ContentUpdaterModal from '../content-tab/components/modals/ContentUpdaterModal.vue'
|
||||||
import ContentDiffModal from './components/ContentDiffModal.vue'
|
import ContentDiffModal from './components/ContentDiffModal.vue'
|
||||||
|
import IncompatibleContentModal from './components/IncompatibleContentModal.vue'
|
||||||
import { useInstallationForm } from './composables'
|
import { useInstallationForm } from './composables'
|
||||||
import { injectInstallationSettings } from './providers/installation-settings'
|
import { injectInstallationSettings } from './providers/installation-settings'
|
||||||
|
|
||||||
@@ -44,11 +46,17 @@ const unlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
|||||||
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>()
|
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>()
|
||||||
|
|
||||||
const contentDiffModal = ref<InstanceType<typeof ContentDiffModal>>()
|
const contentDiffModal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||||
|
const incompatibleContentModal = ref<InstanceType<typeof IncompatibleContentModal>>()
|
||||||
const modpackUpdateModal = ref<InstanceType<typeof ConfirmModpackUpdateModal>>()
|
const modpackUpdateModal = ref<InstanceType<typeof ConfirmModpackUpdateModal>>()
|
||||||
const pendingUpdateVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
const pendingUpdateVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
||||||
const isUpdateDowngrade = ref(false)
|
const isUpdateDowngrade = ref(false)
|
||||||
|
|
||||||
const form = useInstallationForm(ctx, contentUpdaterModal, contentDiffModal)
|
const form = useInstallationForm(
|
||||||
|
ctx,
|
||||||
|
contentUpdaterModal,
|
||||||
|
contentDiffModal,
|
||||||
|
incompatibleContentModal,
|
||||||
|
)
|
||||||
|
|
||||||
function handleBeforeUnload(e: BeforeUnloadEvent) {
|
function handleBeforeUnload(e: BeforeUnloadEvent) {
|
||||||
if (form.isSaving.value) {
|
if (form.isSaving.value) {
|
||||||
@@ -133,6 +141,16 @@ function handleUnlink() {
|
|||||||
ctx.unlinkModpack()
|
ctx.unlinkModpack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'reset-server': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleIncompatibleResetServer() {
|
||||||
|
form.cancelPreview()
|
||||||
|
form.cancelEditing()
|
||||||
|
emit('reset-server')
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
cancelEditing: () => form.cancelEditing(),
|
cancelEditing: () => form.cancelEditing(),
|
||||||
})
|
})
|
||||||
@@ -477,6 +495,8 @@ const messages = defineMessages({
|
|||||||
<Chips
|
<Chips
|
||||||
v-model="form.selectedPlatform.value"
|
v-model="form.selectedPlatform.value"
|
||||||
:items="ctx.availablePlatforms"
|
:items="ctx.availablePlatforms"
|
||||||
|
:format-label="formatLoaderLabel"
|
||||||
|
:capitalize="false"
|
||||||
:disabled-items="disabledPlatforms"
|
:disabled-items="disabledPlatforms"
|
||||||
:disabled-tooltip="formatMessage(messages.platformLockTooltip)"
|
:disabled-tooltip="formatMessage(messages.platformLockTooltip)"
|
||||||
:aria-label="formatMessage(messages.selectPlatformAriaLabel)"
|
:aria-label="formatMessage(messages.selectPlatformAriaLabel)"
|
||||||
@@ -708,8 +728,20 @@ const messages = defineMessages({
|
|||||||
@unlink="handleUnlink"
|
@unlink="handleUnlink"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<IncompatibleContentModal
|
||||||
|
v-if="form.incompatibleContentVariant.value"
|
||||||
|
ref="incompatibleContentModal"
|
||||||
|
:variant="form.incompatibleContentVariant.value"
|
||||||
|
:loading="form.isVerifying.value || form.isSaving.value"
|
||||||
|
@confirm-loader-change="form.confirmLoaderChange()"
|
||||||
|
@auto-fix="form.confirmAutoFix()"
|
||||||
|
@disable-conflicts="form.confirmDisableConflicts()"
|
||||||
|
@reset-server="handleIncompatibleResetServer"
|
||||||
|
@cancel="form.cancelPreview()"
|
||||||
|
/>
|
||||||
|
|
||||||
<ContentDiffModal
|
<ContentDiffModal
|
||||||
v-if="form.pendingPreview.value"
|
v-if="form.pendingPreview.value && !form.incompatibleContentVariant.value"
|
||||||
ref="contentDiffModal"
|
ref="contentDiffModal"
|
||||||
:header="formatMessage(messages.confirmVersionChangeHeader)"
|
:header="formatMessage(messages.confirmVersionChangeHeader)"
|
||||||
:description="
|
:description="
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ComputedRef, Ref } from 'vue'
|
|||||||
import { createContext } from '#ui/providers/create-context'
|
import { createContext } from '#ui/providers/create-context'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ContentDiffItem,
|
||||||
ContentDiffPreview,
|
ContentDiffPreview,
|
||||||
GameVersionOption,
|
GameVersionOption,
|
||||||
InstallationInfoRow,
|
InstallationInfoRow,
|
||||||
@@ -61,6 +62,26 @@ export interface InstallationSettingsContext {
|
|||||||
|
|
||||||
lockPlatform?: boolean
|
lockPlatform?: boolean
|
||||||
hideLoaderVersion?: boolean
|
hideLoaderVersion?: boolean
|
||||||
|
|
||||||
|
/** Bulk-disable all addons on the server (used before switching loaders). */
|
||||||
|
disableAllContent?: () => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable only the incompatible addons identified in a content diff preview.
|
||||||
|
* Used when the user chooses "Disable conflicts" instead of "Auto-fix".
|
||||||
|
*/
|
||||||
|
disableIncompatibleContent?: (diffs: ContentDiffItem[]) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the installation settings without auto-resolving content.
|
||||||
|
* Uses installContent with soft_override instead of applyGameVersionUpdate.
|
||||||
|
*/
|
||||||
|
saveWithoutAutoFix?: (
|
||||||
|
platform: string,
|
||||||
|
gameVersion: string,
|
||||||
|
loaderVersionId: string | null,
|
||||||
|
) => Promise<void>
|
||||||
|
|
||||||
previewSave?: (
|
previewSave?: (
|
||||||
platform: string,
|
platform: string,
|
||||||
gameVersion: string,
|
gameVersion: string,
|
||||||
|
|||||||
@@ -233,6 +233,9 @@
|
|||||||
"content.confirm-bulk-update.header": {
|
"content.confirm-bulk-update.header": {
|
||||||
"defaultMessage": "Update projects"
|
"defaultMessage": "Update projects"
|
||||||
},
|
},
|
||||||
|
"content.confirm-bulk-update.shift-click-hint": {
|
||||||
|
"defaultMessage": "Hold Shift while clicking \"Update all\" to skip this confirmation in the future."
|
||||||
|
},
|
||||||
"content.confirm-bulk-update.update-button": {
|
"content.confirm-bulk-update.update-button": {
|
||||||
"defaultMessage": "Update {count, plural, one {# project} other {# projects}}"
|
"defaultMessage": "Update {count, plural, one {# project} other {# projects}}"
|
||||||
},
|
},
|
||||||
@@ -977,6 +980,30 @@
|
|||||||
"installation-settings.edit.warning-server": {
|
"installation-settings.edit.warning-server": {
|
||||||
"defaultMessage": "We don't recommend editing your installation settings after installing content. If you want to edit them reset your server."
|
"defaultMessage": "We don't recommend editing your installation settings after installing content. If you want to edit them reset your server."
|
||||||
},
|
},
|
||||||
|
"installation-settings.incompatible-content.auto-fix-button": {
|
||||||
|
"defaultMessage": "Auto-fix"
|
||||||
|
},
|
||||||
|
"installation-settings.incompatible-content.change-loader-button": {
|
||||||
|
"defaultMessage": "Change loader"
|
||||||
|
},
|
||||||
|
"installation-settings.incompatible-content.disable-conflicts-button": {
|
||||||
|
"defaultMessage": "Disable conflicts"
|
||||||
|
},
|
||||||
|
"installation-settings.incompatible-content.game-version-warning-body": {
|
||||||
|
"defaultMessage": "When changing the game version, we can either disable incompatible installed content or attempt to resolve the incompatibilities."
|
||||||
|
},
|
||||||
|
"installation-settings.incompatible-content.game-version-warning-title": {
|
||||||
|
"defaultMessage": "Incompatibility warning"
|
||||||
|
},
|
||||||
|
"installation-settings.incompatible-content.header": {
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"installation-settings.incompatible-content.loader-change-title": {
|
||||||
|
"defaultMessage": "Changing loaders is destructive"
|
||||||
|
},
|
||||||
"installation-settings.linked-instance.title": {
|
"installation-settings.linked-instance.title": {
|
||||||
"defaultMessage": "Linked {projectType, select, server {server project} other {modpack}}"
|
"defaultMessage": "Linked {projectType, select, server {server project} other {modpack}}"
|
||||||
},
|
},
|
||||||
|
|||||||
108
packages/ui/src/stories/servers/InstallingBanner.stories.ts
Normal file
108
packages/ui/src/stories/servers/InstallingBanner.stories.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import InstallingBanner from '../../components/servers/InstallingBanner.vue'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Servers/InstallingBanner',
|
||||||
|
component: InstallingBanner,
|
||||||
|
} satisfies Meta<typeof InstallingBanner>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
name: 'Default (no progress)',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithProgress: Story = {
|
||||||
|
args: {
|
||||||
|
progress: {
|
||||||
|
phase: 'InstallingLoader',
|
||||||
|
percent: 45,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InstallingModpack: Story = {
|
||||||
|
args: {
|
||||||
|
progress: {
|
||||||
|
phase: 'InstallingPack',
|
||||||
|
percent: 72,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InstallingAddons: Story = {
|
||||||
|
args: {
|
||||||
|
progress: {
|
||||||
|
phase: 'Addons',
|
||||||
|
percent: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorInvalidVersion: Story = {
|
||||||
|
name: 'Error: Invalid Version',
|
||||||
|
args: {
|
||||||
|
contentError: {
|
||||||
|
step: 'modloader',
|
||||||
|
description: 'the specified version may be incorrect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorUnsupportedVersion: Story = {
|
||||||
|
name: 'Error: Unsupported Version',
|
||||||
|
args: {
|
||||||
|
contentError: {
|
||||||
|
step: 'modloader',
|
||||||
|
description: 'this version is not yet supported',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorInternal: Story = {
|
||||||
|
name: 'Error: Internal',
|
||||||
|
args: {
|
||||||
|
contentError: {
|
||||||
|
step: 'modloader',
|
||||||
|
description: 'internal error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorModpackInstall: Story = {
|
||||||
|
name: 'Error: Modpack Install Failed',
|
||||||
|
args: {
|
||||||
|
contentError: {
|
||||||
|
step: 'modpack',
|
||||||
|
description: 'Failed to install modpack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorModpackNoFile: Story = {
|
||||||
|
name: 'Error: Modpack No Primary File',
|
||||||
|
args: {
|
||||||
|
contentError: {
|
||||||
|
step: 'modpack',
|
||||||
|
description: 'Modpack version has no primary file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllStates: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { InstallingBanner },
|
||||||
|
template: /*html*/ `
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||||
|
<InstallingBanner />
|
||||||
|
<InstallingBanner :progress="{ phase: 'InstallingLoader', percent: 45 }" />
|
||||||
|
<InstallingBanner :content-error="{ step: 'modloader', description: 'the specified version may be incorrect' }" />
|
||||||
|
<InstallingBanner :content-error="{ step: 'modloader', description: 'this version is not yet supported' }" />
|
||||||
|
<InstallingBanner :content-error="{ step: 'modloader', description: 'internal error' }" />
|
||||||
|
<InstallingBanner :content-error="{ step: 'modpack', description: 'Failed to install modpack' }" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user