mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: incompat modal improvement (#6256)
* feat: incompat modal improvement * feat: use ContentUpdaterModal and remove IncompatibilityWarningModal * fix: lint * fix: lint
This commit is contained in:
@@ -79,9 +79,6 @@
|
||||
variant === 'outlined'
|
||||
? 'bg-transparent border border-solid border-button-bg rounded-l-xl border-r-0'
|
||||
: 'bg-surface-4 border-none rounded-xl',
|
||||
{
|
||||
'placeholder:text-sm': type === 'search',
|
||||
},
|
||||
]"
|
||||
@input="onInput"
|
||||
@focus="isFocused = true"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:button-class="buttonClass ?? 'flex flex-col gap-2 justify-start items-start'"
|
||||
:content-class="contentClass"
|
||||
title-wrapper-class="flex flex-col gap-2 justify-start items-start"
|
||||
:open-by-default="!locked && (openByDefault !== undefined ? openByDefault : true)"
|
||||
:open-by-default="openByDefault !== undefined ? openByDefault : true"
|
||||
>
|
||||
<template #title>
|
||||
<slot name="header" :filter="filterType">
|
||||
|
||||
@@ -43,7 +43,14 @@ const buttonClass = computed(() => {
|
||||
const contentClass = computed(() => (isApp.value ? 'mt-2 mb-3' : 'mb-4 mx-3'))
|
||||
const innerPanelClass = computed(() => (isApp.value ? 'ml-2 mr-3' : 'p-1'))
|
||||
|
||||
function hasProvidedFilter(filterId: string): boolean {
|
||||
return (ctx.providedFilters?.value ?? []).some((filter) => filter.type === filterId)
|
||||
}
|
||||
|
||||
function getFilterOpenByDefault(filterId: string): boolean {
|
||||
if (hasProvidedFilter(filterId)) {
|
||||
return true
|
||||
}
|
||||
if (ctx.isServerType.value) {
|
||||
return ![
|
||||
'server_category_minecraft_server_meta',
|
||||
|
||||
+7
-3
@@ -8,12 +8,12 @@
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition type="warning" :header="formatMessage(messages.admonitionHeader)">
|
||||
{{ formatMessage(messages.admonitionBody, { count: props.count }) }}
|
||||
{{ formatMessage(messages.admonitionBody, { count: visibleCount }) }}
|
||||
</Admonition>
|
||||
<InlineBackupCreator
|
||||
ref="backupCreator"
|
||||
:backup-name="
|
||||
props.backupTip ? `Before bulk update (${props.backupTip})` : 'Before bulk update'
|
||||
visibleBackupTip ? `Before bulk update (${visibleBackupTip})` : 'Before bulk update'
|
||||
"
|
||||
:shift-click-hint-override="formatMessage(messages.shiftClickHint)"
|
||||
@update:buttons-disabled="buttonsDisabled = $event"
|
||||
@@ -35,7 +35,7 @@
|
||||
@click="confirm"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.updateButton, { count: props.count }) }}
|
||||
{{ formatMessage(messages.updateButton, { count: visibleCount }) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -97,8 +97,12 @@ const emit = defineEmits<{
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const backupCreator = ref<InstanceType<typeof InlineBackupCreator>>()
|
||||
const buttonsDisabled = ref(false)
|
||||
const visibleCount = ref(props.count)
|
||||
const visibleBackupTip = ref(props.backupTip)
|
||||
|
||||
function show() {
|
||||
visibleCount.value = props.count
|
||||
visibleBackupTip.value = props.backupTip
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -3,7 +3,7 @@
|
||||
ref="modal"
|
||||
:header="
|
||||
formatMessage(messages.header, {
|
||||
itemType: formatContentTypeSentence(formatMessage, props.itemType, props.count),
|
||||
itemType: formatContentTypeSentence(formatMessage, visibleItemType, visibleCount),
|
||||
})
|
||||
"
|
||||
:fade="props.variant === 'server' ? 'warning' : 'danger'"
|
||||
@@ -41,8 +41,8 @@
|
||||
<TrashIcon />
|
||||
{{
|
||||
formatMessage(messages.deleteButton, {
|
||||
count: props.count,
|
||||
itemType: formatContentTypeSentence(formatMessage, props.itemType, props.count),
|
||||
count: visibleCount,
|
||||
itemType: formatContentTypeSentence(formatMessage, visibleItemType, visibleCount),
|
||||
})
|
||||
}}
|
||||
</button>
|
||||
@@ -110,8 +110,12 @@ const emit = defineEmits<{
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const backupCreator = ref<InstanceType<typeof InlineBackupCreator>>()
|
||||
const buttonsDisabled = ref(false)
|
||||
const visibleCount = ref(props.count)
|
||||
const visibleItemType = ref(props.itemType)
|
||||
|
||||
function show() {
|
||||
visibleCount.value = props.count
|
||||
visibleItemType.value = props.itemType
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
|
||||
+20
-10
@@ -98,14 +98,10 @@
|
||||
v-for="inst in filteredInstances"
|
||||
:key="inst.id"
|
||||
class="flex items-center justify-between px-6 py-1.5"
|
||||
:class="
|
||||
!inst.compatible ? 'opacity-40' : inst.installed ? 'opacity-60' : 'hover:bg-surface-3'
|
||||
"
|
||||
:class="inst.installed ? 'opacity-60' : 'hover:bg-surface-3'"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
!inst.compatible ? 'This instance is not compatible with this project' : undefined
|
||||
"
|
||||
v-tooltip="!inst.compatible ? formatMessage(messages.incompatibleTooltip) : undefined"
|
||||
class="flex min-w-0 cursor-pointer items-center gap-2.5 overflow-hidden border-0 bg-transparent p-0 text-left"
|
||||
@click="emit('navigate', inst)"
|
||||
>
|
||||
@@ -120,8 +116,17 @@
|
||||
{{ formatMessage(messages.installedBadge) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="inst.compatible">
|
||||
<button :disabled="inst.installing" @click="emit('install', inst)">
|
||||
<ButtonStyled
|
||||
v-else
|
||||
:type="inst.compatible ? 'standard' : 'outlined'"
|
||||
:color="inst.compatible ? 'standard' : 'orange'"
|
||||
>
|
||||
<button
|
||||
v-tooltip="!inst.compatible ? formatMessage(messages.incompatibleTooltip) : undefined"
|
||||
:disabled="inst.installing"
|
||||
@click="emit('install', inst)"
|
||||
>
|
||||
<TriangleAlertIcon v-if="!inst.compatible" />
|
||||
{{
|
||||
inst.installing
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
@@ -247,6 +252,7 @@ import {
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
SearchIcon,
|
||||
TriangleAlertIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
@@ -296,6 +302,11 @@ const messages = defineMessages({
|
||||
id: 'instances.content-install.install-button',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
incompatibleTooltip: {
|
||||
id: 'instances.content-install.incompatible-tooltip',
|
||||
defaultMessage:
|
||||
'This instance uses a different loader or game version than this project supports.',
|
||||
},
|
||||
selectIcon: {
|
||||
id: 'instances.content-install.select-icon',
|
||||
defaultMessage: 'Select icon',
|
||||
@@ -452,7 +463,7 @@ function removeIcon() {
|
||||
function resetState() {
|
||||
tab.value = props.defaultTab ?? 'existing'
|
||||
searchFilter.value = ''
|
||||
hideUninstallable.value = true
|
||||
hideUninstallable.value = false
|
||||
instanceName.value = `New instance (${props.instances.length + 1})`
|
||||
iconPath.value = null
|
||||
iconPreviewUrl.value = null
|
||||
@@ -471,7 +482,6 @@ function resetState() {
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
resetState()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
|
||||
+112
-32
@@ -3,20 +3,12 @@
|
||||
ref="modal"
|
||||
:max-width="'min(928px, calc(95vw - 10rem))'"
|
||||
:width="'min(928px, calc(95vw - 10rem))'"
|
||||
:on-hide="handleModalHide"
|
||||
no-padding
|
||||
>
|
||||
<template #title>
|
||||
<Avatar v-if="projectIconUrl" :src="projectIconUrl" size="3rem" :tint-by="projectName" />
|
||||
<span class="text-lg font-extrabold text-contrast">{{
|
||||
header ??
|
||||
formatMessage(
|
||||
isModpack
|
||||
? messages.switchModpackVersionHeader
|
||||
: switchMode
|
||||
? messages.switchVersionHeader
|
||||
: messages.updateVersionHeader,
|
||||
)
|
||||
}}</span>
|
||||
<span class="text-lg font-extrabold text-contrast">{{ header ?? defaultHeader }}</span>
|
||||
</template>
|
||||
<div
|
||||
class="flex h-[min(550px,calc(95vh-10rem))] border-solid border-transparent border-[1px] border-b-surface-4"
|
||||
@@ -99,7 +91,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isModpack"
|
||||
v-if="!isModpack && !incompatibilityWarningMode"
|
||||
class="absolute bottom-0 left-0 right-0 pointer-events-none flex flex-col items-center justify-end bg-gradient-to-b from-transparent to-bg-raised to-70% pb-3 h-24"
|
||||
>
|
||||
<div class="pointer-events-auto">
|
||||
@@ -204,7 +196,14 @@
|
||||
>
|
||||
<TriangleAlertIcon class="size-6 shrink-0" />
|
||||
<span>{{
|
||||
formatMessage(isApp ? messages.updateWarningApp : messages.updateWarningWeb)
|
||||
warning ??
|
||||
formatMessage(
|
||||
incompatibilityWarningMode
|
||||
? messages.incompatibilityWarning
|
||||
: isApp
|
||||
? messages.updateWarningApp
|
||||
: messages.updateWarningWeb,
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 shrink-0 ml-auto">
|
||||
@@ -214,26 +213,34 @@
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<ButtonStyled :color="incompatibilityWarningMode ? 'orange' : 'brand'">
|
||||
<button
|
||||
v-tooltip="props.actionDisabled ? props.actionDisabledTooltip : undefined"
|
||||
:disabled="
|
||||
props.actionDisabled || !selectedVersion || selectedVersion.id === currentVersionId
|
||||
actionLoading ||
|
||||
props.actionDisabled ||
|
||||
!selectedVersion ||
|
||||
(!incompatibilityWarningMode && selectedVersion.id === currentVersionId)
|
||||
"
|
||||
@click="handleUpdate"
|
||||
>
|
||||
<DownloadIcon />
|
||||
<SpinnerIcon v-if="actionLoading" class="size-5 animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
formatMessage(
|
||||
isDowngrade
|
||||
? messages.downgradeToVersion
|
||||
: switchMode
|
||||
? messages.switchToVersion
|
||||
: messages.updateToVersion,
|
||||
{
|
||||
version: selectedVersion?.version_number ?? '...',
|
||||
},
|
||||
)
|
||||
actionLoading
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: incompatibilityWarningMode
|
||||
? formatMessage(messages.installAnywayButton)
|
||||
: formatMessage(
|
||||
isDowngrade
|
||||
? messages.downgradeToVersion
|
||||
: switchMode
|
||||
? messages.switchToVersion
|
||||
: messages.updateToVersion,
|
||||
{
|
||||
version: selectedVersion?.version_number ?? '...',
|
||||
},
|
||||
)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -270,7 +277,12 @@ import {
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { capitalizeString, renderHighlightedString } from '@modrinth/utils'
|
||||
import {
|
||||
capitalizeString,
|
||||
formatVersionsForDisplay,
|
||||
type GameVersionTag,
|
||||
renderHighlightedString,
|
||||
} from '@modrinth/utils'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
@@ -282,6 +294,7 @@ import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import VersionChannelIndicator from '#ui/components/version/VersionChannelIndicator.vue'
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectTags } from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import {
|
||||
versionChangesGameVersion,
|
||||
@@ -290,12 +303,17 @@ import {
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const debug = useDebugLogger('ContentUpdaterModal')
|
||||
const tags = injectTags(null)
|
||||
|
||||
const messages = defineMessages({
|
||||
updateVersionHeader: {
|
||||
id: 'instances.updater-modal.header',
|
||||
defaultMessage: 'Update version',
|
||||
},
|
||||
incompatibilityWarningHeader: {
|
||||
id: 'instances.updater-modal.incompatibility-warning-header',
|
||||
defaultMessage: 'Choose version',
|
||||
},
|
||||
switchModpackVersionHeader: {
|
||||
id: 'instances.updater-modal.header-modpack',
|
||||
defaultMessage: 'Switch modpack version',
|
||||
@@ -333,6 +351,11 @@ const messages = defineMessages({
|
||||
id: 'instances.updater-modal.warning-web',
|
||||
defaultMessage: 'Updating can break your world. Review version changelogs and back up first.',
|
||||
},
|
||||
incompatibilityWarning: {
|
||||
id: 'instances.updater-modal.incompatibility-warning',
|
||||
defaultMessage:
|
||||
'This version is not marked as compatible with this instance. Dependencies will not be installed automatically.',
|
||||
},
|
||||
downgradeToVersion: {
|
||||
id: 'instances.updater-modal.downgrade-to',
|
||||
defaultMessage: 'Downgrade to {version}',
|
||||
@@ -378,6 +401,10 @@ const messages = defineMessages({
|
||||
id: 'instances.updater-modal.incompatible-update.proceed',
|
||||
defaultMessage: 'Update anyway',
|
||||
},
|
||||
installAnywayButton: {
|
||||
id: 'instances.updater-modal.install-anyway',
|
||||
defaultMessage: 'Install anyway',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -392,6 +419,9 @@ const props = withDefaults(
|
||||
projectIconUrl?: string
|
||||
projectName?: string
|
||||
header?: string
|
||||
mode?: 'version' | 'incompatibility-warning'
|
||||
warning?: string
|
||||
actionLoading?: boolean
|
||||
/** Whether versions are currently being loaded */
|
||||
loading?: boolean
|
||||
/** Whether changelog is being loaded for the selected version */
|
||||
@@ -404,6 +434,9 @@ const props = withDefaults(
|
||||
projectIconUrl: undefined,
|
||||
projectName: undefined,
|
||||
header: undefined,
|
||||
mode: 'version',
|
||||
warning: undefined,
|
||||
actionLoading: false,
|
||||
loading: false,
|
||||
loadingChangelog: false,
|
||||
actionDisabled: false,
|
||||
@@ -412,6 +445,20 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const isModpack = computed(() => props.projectType === 'modpack')
|
||||
const incompatibilityWarningMode = computed(() => props.mode === 'incompatibility-warning')
|
||||
const defaultHeader = computed(() => {
|
||||
if (incompatibilityWarningMode.value) {
|
||||
return formatMessage(messages.incompatibilityWarningHeader)
|
||||
}
|
||||
|
||||
return formatMessage(
|
||||
isModpack.value
|
||||
? messages.switchModpackVersionHeader
|
||||
: switchMode.value
|
||||
? messages.switchVersionHeader
|
||||
: messages.updateVersionHeader,
|
||||
)
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [version: Labrinth.Versions.v2.Version, event: MouseEvent]
|
||||
@@ -431,6 +478,7 @@ const pendingIncompatibleUpdate = ref<{
|
||||
version: Labrinth.Versions.v2.Version
|
||||
event: MouseEvent
|
||||
} | null>(null)
|
||||
const suppressCancelOnHide = ref(false)
|
||||
// Store the initial version ID to select when versions become available
|
||||
const pendingInitialVersionId = ref<string | undefined>(undefined)
|
||||
const pinnedInitialVersionId = ref<string | undefined>(undefined)
|
||||
@@ -509,12 +557,16 @@ const filteredVersions = computed(() => {
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
versions = versions.filter(
|
||||
(v) => v.name.toLowerCase().includes(query) || v.version_number.toLowerCase().includes(query),
|
||||
(v) =>
|
||||
v.name.toLowerCase().includes(query) ||
|
||||
v.version_number.toLowerCase().includes(query) ||
|
||||
(incompatibilityWarningMode.value &&
|
||||
[...v.loaders, ...v.game_versions].some((value) => value.toLowerCase().includes(query))),
|
||||
)
|
||||
}
|
||||
|
||||
const beforeFilterCount = versions.length
|
||||
if (!isModpack.value && hideIncompatibleState.value) {
|
||||
if (!incompatibilityWarningMode.value && !isModpack.value && hideIncompatibleState.value) {
|
||||
versions = versions.filter(
|
||||
(version) =>
|
||||
version.id === props.currentVersionId ||
|
||||
@@ -537,6 +589,7 @@ const filteredVersions = computed(() => {
|
||||
})
|
||||
|
||||
function shouldShowBadge(version: Labrinth.Versions.v2.Version): boolean {
|
||||
if (incompatibilityWarningMode.value) return false
|
||||
return version.id === props.currentVersionId || shouldShowIncompatibleBadge(version)
|
||||
}
|
||||
|
||||
@@ -596,8 +649,20 @@ function formatLongDate(dateString: string): string {
|
||||
|
||||
function formatLoaderGameVersion(version: Labrinth.Versions.v2.Version): string {
|
||||
const loader = capitalizeString(version.loaders[0] || '')
|
||||
const gameVersion = version.game_versions[0] || ''
|
||||
return `${loader} ${gameVersion}`
|
||||
const gameVersions = formatGameVersions(version)
|
||||
return [loader, gameVersions].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function formatGameVersions(version: Labrinth.Versions.v2.Version): string {
|
||||
if (!incompatibilityWarningMode.value) {
|
||||
return version.game_versions[0] || ''
|
||||
}
|
||||
|
||||
const gameVersions = tags?.gameVersions.value?.length
|
||||
? formatVersionsForDisplay(version.game_versions, tags.gameVersions.value as GameVersionTag[])
|
||||
: version.game_versions
|
||||
|
||||
return gameVersions.join(', ')
|
||||
}
|
||||
|
||||
let prefetchTimeout: ReturnType<typeof useTimeoutFn> | null = null
|
||||
@@ -623,8 +688,13 @@ function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
|
||||
}
|
||||
|
||||
function handleUpdate(event: MouseEvent) {
|
||||
if (props.actionDisabled) return
|
||||
if (props.actionLoading || props.actionDisabled) return
|
||||
if (selectedVersion.value) {
|
||||
if (incompatibilityWarningMode.value) {
|
||||
emitUpdate(selectedVersion.value, event, { hide: false })
|
||||
return
|
||||
}
|
||||
|
||||
const changesGameVersion = versionChangesGameVersion(
|
||||
selectedVersion.value,
|
||||
props.currentGameVersion,
|
||||
@@ -689,9 +759,18 @@ function handleCancel() {
|
||||
hide()
|
||||
}
|
||||
|
||||
function handleModalHide() {
|
||||
if (suppressCancelOnHide.value) {
|
||||
suppressCancelOnHide.value = false
|
||||
return
|
||||
}
|
||||
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function show(initialVersionId?: string, options?: { switchMode?: boolean }) {
|
||||
searchQuery.value = ''
|
||||
hideIncompatibleState.value = !isModpack.value
|
||||
hideIncompatibleState.value = incompatibilityWarningMode.value ? false : !isModpack.value
|
||||
pendingIncompatibleUpdate.value = null
|
||||
pinnedInitialVersionId.value = initialVersionId
|
||||
switchMode.value = options?.switchMode ?? false
|
||||
@@ -735,6 +814,7 @@ function show(initialVersionId?: string, options?: { switchMode?: boolean }) {
|
||||
}
|
||||
|
||||
function hide() {
|
||||
suppressCancelOnHide.value = true
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
|
||||
@@ -1646,6 +1646,9 @@
|
||||
"instances.content-install.header": {
|
||||
"defaultMessage": "Install project"
|
||||
},
|
||||
"instances.content-install.incompatible-tooltip": {
|
||||
"defaultMessage": "This instance uses a different loader or game version than this project supports."
|
||||
},
|
||||
"instances.content-install.install-button": {
|
||||
"defaultMessage": "Install"
|
||||
},
|
||||
@@ -1718,6 +1721,12 @@
|
||||
"instances.updater-modal.hide-incompatible": {
|
||||
"defaultMessage": "Hide incompatible"
|
||||
},
|
||||
"instances.updater-modal.incompatibility-warning": {
|
||||
"defaultMessage": "This version is not marked as compatible with this instance. Dependencies will not be installed automatically."
|
||||
},
|
||||
"instances.updater-modal.incompatibility-warning-header": {
|
||||
"defaultMessage": "Choose version"
|
||||
},
|
||||
"instances.updater-modal.incompatible-update.description": {
|
||||
"defaultMessage": "{version} is not marked as compatible with this installation. It may fail to launch or behave unexpectedly."
|
||||
},
|
||||
@@ -1727,6 +1736,9 @@
|
||||
"instances.updater-modal.incompatible-update.proceed": {
|
||||
"defaultMessage": "Update anyway"
|
||||
},
|
||||
"instances.updater-modal.install-anyway": {
|
||||
"defaultMessage": "Install anyway"
|
||||
},
|
||||
"instances.updater-modal.loading-changelog": {
|
||||
"defaultMessage": "Loading changelog..."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user