fix: final content tab qa (#5611)

* fix: queued admonition always showing

* fix: dont apply grayscale to checkbox in content card item

* fix: actual stable id for disable/enable/bulk state

* fix: vue-router resolve workaround

* fix: show disable/enable btns same time

* fix: remove mr-2 on toggle

* fix: type errors + add ModpackAlreadyInstalledModal

* fix: bulk actions + overflow menu hitting ad container

* fix: responsiveness of ContentSelectionBar

* feat: better backup naming for inline backups + sorting fixes

* fix: lint

* fix: typo
This commit is contained in:
Calum H.
2026-03-18 18:03:55 +00:00
committed by GitHub
parent cf1b5f5e2d
commit 1d10af09f5
35 changed files with 503 additions and 215 deletions

View File

@@ -1,11 +1,48 @@
<script setup lang="ts">
import { onUnmounted, watch } from 'vue'
import { onUnmounted, ref, watch } from 'vue'
const props = defineProps<{
shown: boolean
ariaLabel?: string
}>()
const toolbarEl = ref<HTMLElement | null>(null)
const compact = ref(false)
function checkCompact() {
const el = toolbarEl.value
if (!el) return
const clone = el.cloneNode(true) as HTMLElement
clone.classList.remove('bar-compact')
clone.style.position = 'absolute'
clone.style.visibility = 'hidden'
clone.style.pointerEvents = 'none'
clone.style.width = `${el.offsetWidth}px`
el.parentElement!.appendChild(clone)
const needsCompact = clone.offsetHeight > 70
clone.remove()
compact.value = needsCompact
}
let observer: ResizeObserver | null = null
watch(
toolbarEl,
(el) => {
observer?.disconnect()
if (!el) return
observer = new ResizeObserver(() => {
checkCompact()
})
observer.observe(el.parentElement!)
checkCompact()
},
{ immediate: true },
)
watch(
() => props.shown,
(shown) => {
@@ -15,6 +52,7 @@ watch(
)
onUnmounted(() => {
observer?.disconnect()
document?.body.classList.remove('floating-action-bar-shown')
})
</script>
@@ -27,9 +65,11 @@ onUnmounted(() => {
aria-live="polite"
>
<div
ref="toolbarEl"
role="toolbar"
:aria-label="ariaLabel"
class="relative overflow-clip flex items-center gap-2 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto max-w-[60vw] px-4 py-3 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]"
:class="{ 'bar-compact': compact }"
>
<slot />
</div>
@@ -81,4 +121,12 @@ onUnmounted(() => {
.intercom-lightweight-app-launcher {
z-index: 9 !important;
}
.bar-compact .bar-label {
display: none;
}
.bar-compact .cq-show-icon {
display: block;
}
</style>

View File

@@ -100,15 +100,17 @@
<InlineBackupCreator
v-if="ctx.flowType === 'reset-server'"
backup-name="Before reinstall"
ref="backupCreator"
backup-name="Before reset server"
hide-shift-click-hint
@update:buttons-disabled="ctx.isBackingUp.value = $event"
/>
</div>
</template>
<script setup lang="ts">
import { EyeIcon, EyeOffIcon, SettingsIcon } from '@modrinth/assets'
import { computed, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useDebugLogger } from '#ui/composables/debug-logger'
@@ -125,6 +127,11 @@ import { capitalize } from '../shared'
const debug = useDebugLogger('FinalConfigStage')
const ctx = injectCreationFlowContext()
const backupCreator = ref<InstanceType<typeof InlineBackupCreator> | null>(null)
watch(backupCreator, (creator) => {
ctx.cancelBackup.value = creator?.cancelBackup ?? null
})
const {
worldName,
gamemode,

View File

@@ -115,6 +115,10 @@ export interface CreationFlowContextValue {
// Loading state (set when finish() is called, cleared on reset)
loading: Ref<boolean>
// Backup state (set by InlineBackupCreator in reset-server flow)
isBackingUp: Ref<boolean>
cancelBackup: Ref<(() => void) | null>
// Modal
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>
stageConfigs: StageConfigInput<CreationFlowContextValue>[]
@@ -240,6 +244,8 @@ export function createCreationFlowContext(
const hardReset = ref(isInitialSetup)
const loading = ref(false)
const isBackingUp = ref(false)
const cancelBackup = ref<(() => void) | null>(null)
// hideLoaderChips: hides the entire loader chips section (only for vanilla world type in world/server flows)
const hideLoaderChips = computed(() => setupType.value === 'vanilla')
@@ -292,6 +298,8 @@ export function createCreationFlowContext(
hardReset.value = isInitialSetup
loading.value = false
isBackingUp.value = false
cancelBackup.value = null
}
function setSetupType(type: SetupType) {
@@ -401,6 +409,8 @@ export function createCreationFlowContext(
importSearchQuery,
hardReset,
loading,
isBackingUp,
cancelBackup,
modal,
stageConfigs: resolvedStageConfigs,
onBack,

View File

@@ -5,7 +5,7 @@
:context="ctx"
:fade="fade"
disable-progress
@hide="$emit('hide')"
@hide="handleHide"
/>
</template>
@@ -89,5 +89,10 @@ function hide() {
modal.value?.hide()
}
function handleHide() {
ctx.cancelBackup.value?.()
emit('hide')
}
defineExpose({ show, hide, ctx })
</script>

View File

@@ -44,7 +44,7 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
icon: isFinish ? PlusIcon : RightArrowIcon,
iconPosition: isFinish ? ('before' as const) : ('after' as const),
color: isReset ? ('red' as const) : isFinish ? ('brand' as const) : undefined,
disabled: isForwardBlocked(ctx),
disabled: isForwardBlocked(ctx) || ctx.isBackingUp.value,
loading: isFinish && ctx.loading.value,
onClick: () => {
if (isFinish) {

View File

@@ -109,7 +109,7 @@ const description = computed(() => {
const messages = defineMessages({
fallbackName: {
id: 'servers.backups.admonition.fallback-name',
defaultMessage: 'your backup',
defaultMessage: 'Your backup',
},
backupQueuedTitle: {
id: 'servers.backups.admonition.backup-queued.title',

View File

@@ -84,10 +84,10 @@ const admonitions = computed<AdmonitionEntry[]>(() => {
// 1. Active WS entries (real-time progress from backupsState)
for (const [id, entry] of backupsState.entries()) {
const backup = findBackup(id)
seenIds.add(id)
if (entry.create && entry.create.state === 'ongoing') {
const key = `${id}:create`
if (!dismissedIds.has(key)) {
seenIds.add(id)
result.push({
key,
backupId: id,
@@ -102,7 +102,6 @@ const admonitions = computed<AdmonitionEntry[]>(() => {
if (entry.restore && entry.restore.state === 'ongoing') {
const key = `${id}:restore`
if (!dismissedIds.has(key)) {
seenIds.add(id)
result.push({
key,
backupId: id,

View File

@@ -178,10 +178,10 @@ const calculateMenuPosition = () => {
top = Math.max(margin, window.innerHeight - menuHeight - margin)
}
if (triggerRect.left + menuWidth + margin <= window.innerWidth) {
left = triggerRect.left
} else if (triggerRect.right - menuWidth - margin >= 0) {
if (triggerRect.right - menuWidth >= margin) {
left = triggerRect.right - menuWidth
} else if (triggerRect.left + menuWidth + margin <= window.innerWidth) {
left = triggerRect.left
} else {
left = Math.max(margin, window.innerWidth - menuWidth - margin)
}

View File

@@ -87,22 +87,23 @@ const deleteHovered = ref(false)
:class="{ 'opacity-50': disabled }"
>
<div
class="flex min-w-0 items-center gap-4 transition-[filter,opacity] duration-200"
:class="[
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none',
enabled === false && !disabled ? 'grayscale opacity-50' : '',
]"
class="flex min-w-0 items-center gap-4"
:class="
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none'
"
>
<Checkbox
v-if="showCheckbox"
:model-value="selected ?? false"
:disabled="disabled"
:aria-label="`Select ${project.title}`"
class="shrink-0"
@update:model-value="selected = $event"
/>
<div class="flex min-w-0 items-center gap-3">
<div
class="flex min-w-0 items-center gap-3 transition-[filter,opacity] duration-200"
:class="enabled === false && !disabled ? 'grayscale opacity-50' : ''"
>
<div
v-tooltip="installing ? formatMessage(commonMessages.installingLabel) : undefined"
class="relative shrink-0"
@@ -256,7 +257,7 @@ const deleteHovered = ref(false)
:model-value="enabled"
:disabled="disabled"
:aria-label="project.title"
class="mr-2 my-auto"
class="my-auto"
@update:model-value="(val) => emit('update:enabled', val as boolean)"
/>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { PowerIcon, PowerOffIcon } from '@modrinth/assets'
import { PowerIcon, PowerOffIcon, XIcon } from '@modrinth/assets'
import { computed } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
@@ -61,6 +61,14 @@ const messages = defineMessages({
id: 'content.selection-bar.bulk.deleting-waiting',
defaultMessage: 'Deleting {contentType}...',
},
allAlreadyEnabled: {
id: 'content.selection-bar.all-already-enabled',
defaultMessage: 'All selected content is already enabled',
},
allAlreadyDisabled: {
id: 'content.selection-bar.all-already-disabled',
defaultMessage: 'All selected content is already disabled',
},
})
interface Props {
@@ -95,6 +103,7 @@ const emit = defineEmits<{
const shown = computed(() => props.selectedItems.length > 0 || props.isBulkOperating)
const allDisabled = computed(() => props.selectedItems.every((m) => !m.enabled))
const allEnabled = computed(() => props.selectedItems.every((m) => m.enabled))
const selectedCountText = computed(() => {
const count = props.selectedItems.length || props.bulkTotal
@@ -135,12 +144,14 @@ const bulkProgressMessage = computed(() => {
<div class="mx-1 h-6 w-px bg-surface-5" />
<ButtonStyled type="transparent">
<button
v-tooltip="formatMessage(commonMessages.clearButton)"
class="!text-primary"
:disabled="isBulkOperating"
:class="{ 'opacity-60 pointer-events-none': isBulkOperating }"
@click="emit('clear')"
>
{{ formatMessage(commonMessages.clearButton) }}
<XIcon class="hidden cq-show-icon" />
<span class="bar-label">{{ formatMessage(commonMessages.clearButton) }}</span>
</button>
</ButtonStyled>
</div>
@@ -148,16 +159,30 @@ const bulkProgressMessage = computed(() => {
<div v-if="!isBulkOperating" class="ml-auto flex items-center gap-0.5">
<slot name="actions" />
<ButtonStyled v-if="allDisabled" type="transparent">
<button :disabled="isBusy" @click="emit('enable')">
<ButtonStyled type="transparent">
<button
v-tooltip="
allEnabled ? formatMessage(messages.allAlreadyEnabled) : formatMessage(messages.enable)
"
:disabled="isBusy || allEnabled"
@click="emit('enable')"
>
<PowerIcon />
{{ formatMessage(messages.enable) }}
<span class="bar-label">{{ formatMessage(messages.enable) }}</span>
</button>
</ButtonStyled>
<ButtonStyled v-else type="transparent">
<button :disabled="isBusy" @click="emit('disable')">
<ButtonStyled type="transparent">
<button
v-tooltip="
allDisabled
? formatMessage(messages.allAlreadyDisabled)
: formatMessage(messages.disable)
"
:disabled="isBusy || allDisabled"
@click="emit('disable')"
>
<PowerOffIcon />
{{ formatMessage(messages.disable) }}
<span class="bar-label">{{ formatMessage(messages.disable) }}</span>
</button>
</ButtonStyled>

View File

@@ -12,7 +12,7 @@
</Admonition>
<InlineBackupCreator
ref="backupCreator"
backup-name="Before bulk update"
:backup-name="backupTip ? `Before bulk update (${backupTip})` : 'Before bulk update'"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -73,6 +73,7 @@ const messages = defineMessages({
defineProps<{
count: number
server?: boolean
backupTip?: string
}>()
const emit = defineEmits<{

View File

@@ -15,7 +15,7 @@
</Admonition>
<InlineBackupCreator
ref="backupCreator"
backup-name="Before deletion"
:backup-name="backupTip ? `Before deletion (${backupTip})` : 'Before deletion'"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -78,9 +78,11 @@ withDefaults(
count: number
itemType: string
variant?: 'instance' | 'server'
backupTip?: string
}>(),
{
variant: 'instance',
backupTip: undefined,
},
)

View File

@@ -17,7 +17,7 @@
</Admonition>
<InlineBackupCreator
ref="backupCreator"
:backup-name="downgrade ? 'Before modpack downgrade' : 'Before modpack update'"
:backup-name="backupName"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -45,7 +45,7 @@
<script setup lang="ts">
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
@@ -55,13 +55,21 @@ import { commonMessages } from '#ui/utils/common-messages'
import InlineBackupCreator from './InlineBackupCreator.vue'
defineProps<{
const props = defineProps<{
downgrade?: boolean
server?: boolean
backupTip?: string
}>()
const { formatMessage } = useVIntl()
const backupName = computed(() => {
const action = props.downgrade ? 'downgrade' : 'update'
return props.backupTip
? `Before modpack ${action} (${props.backupTip})`
: `Before modpack ${action}`
})
const messages = defineMessages({
header: {
id: 'content.confirm-modpack-update.header',

View File

@@ -12,7 +12,7 @@
</Admonition>
<InlineBackupCreator
ref="backupCreator"
backup-name="Before reinstall"
:backup-name="backupTip ? `Before reinstall (${backupTip})` : 'Before reinstall'"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -72,6 +72,7 @@ const messages = defineMessages({
defineProps<{
server?: boolean
backupTip?: string
}>()
const emit = defineEmits<{

View File

@@ -12,7 +12,7 @@
</Admonition>
<InlineBackupCreator
ref="backupCreator"
backup-name="Before unlink"
:backup-name="backupTip ? `Before unlink (${backupTip})` : 'Before unlink'"
@update:buttons-disabled="buttonsDisabled = $event"
/>
</div>
@@ -50,6 +50,7 @@ import InlineBackupCreator from './InlineBackupCreator.vue'
defineProps<{
server?: boolean
backupTip?: string
}>()
const { formatMessage } = useVIntl()

View File

@@ -3,19 +3,16 @@ import { computed, ref, watch } from 'vue'
import type { ContentItem } from '../types'
export function useContentSelection(
items: Ref<ContentItem[]>,
getItemId: (item: ContentItem) => string,
) {
export function useContentSelection(items: Ref<ContentItem[]>) {
const selectedIds = ref<string[]>([])
const selectedItems = computed(() =>
items.value.filter((item) => selectedIds.value.includes(getItemId(item))),
items.value.filter((item) => selectedIds.value.includes(item.id)),
)
watch(items, (newItems) => {
if (selectedIds.value.length === 0) return
const validIds = new Set(newItems.map(getItemId))
const validIds = new Set(newItems.map((item) => item.id))
const pruned = selectedIds.value.filter((id) => validIds.has(id))
if (pruned.length !== selectedIds.value.length) {
selectedIds.value = pruned

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import {
ArrowDownAZIcon,
ArrowDownZAIcon,
ArrowUpZAIcon,
ClockArrowDownIcon,
ClockArrowUpIcon,
CodeIcon,
@@ -171,8 +171,8 @@ const sortLabels: Record<SortMode, () => string> = {
function cycleSortMode() {
const modes: SortMode[] = [
'alphabetical-asc',
'date-added-newest',
'alphabetical-desc',
'date-added-newest',
'date-added-oldest',
]
const idx = modes.indexOf(sortMode.value)
@@ -235,7 +235,6 @@ const { selectedFilters, filterOptions, toggleFilter, applyFilters } = useConten
const { selectedIds, selectedItems, clearSelection, removeFromSelection } = useContentSelection(
ctx.items,
ctx.getItemId,
)
const { isBulkOperating, bulkProgress, bulkTotal, bulkOperation, runBulk } = useBulkOperation()
@@ -292,7 +291,7 @@ const pendingDeletionItems = ref<ContentItem[]>([])
const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>()
function handleDeleteById(id: string, event?: MouseEvent) {
const item = ctx.items.value.find((i) => ctx.getItemId(i) === id)
const item = ctx.items.value.find((i) => i.id === id)
if (item) {
pendingDeletionItems.value = [item]
if (event?.shiftKey) {
@@ -334,7 +333,7 @@ async function confirmDelete() {
if (itemsToDelete.length === 1) {
const item = itemsToDelete[0]
const id = ctx.getItemId(item)
const id = item.id
markChanging(id)
await ctx.deleteItem(item)
removeFromSelection(id)
@@ -347,14 +346,14 @@ async function confirmDelete() {
itemsToDelete,
async (item) => {
await ctx.deleteItem(item)
removeFromSelection(ctx.getItemId(item))
removeFromSelection(item.id)
},
{ onComplete: clearSelection },
)
}
async function handleToggleEnabledById(id: string, _value: boolean) {
const item = ctx.items.value.find((i) => ctx.getItemId(i) === id)
const item = ctx.items.value.find((i) => i.id === id)
if (!item) return
markChanging(id)
try {
@@ -647,7 +646,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
"
@click="cycleSortMode"
>
<ArrowDownZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
v-else-if="sortMode === 'date-added-newest'"
/><ClockArrowUpIcon
v-else-if="sortMode === 'date-added-oldest'"
@@ -667,7 +666,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
"
@click="cycleSortMode"
>
<ArrowDownZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
v-else-if="sortMode === 'date-added-newest'"
/><ClockArrowUpIcon
v-else-if="sortMode === 'date-added-oldest'"
@@ -790,9 +789,13 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
color-fill="text"
hover-color-fill="background"
>
<button :disabled="ctx.isBusy.value" @click="promptUpdateSelected">
<button
v-tooltip="formatMessage(commonMessages.updateButton)"
:disabled="ctx.isBusy.value"
@click="promptUpdateSelected"
>
<DownloadIcon />
{{ formatMessage(commonMessages.updateButton) }}
<span class="bar-label">{{ formatMessage(commonMessages.updateButton) }}</span>
</button>
</ButtonStyled>
@@ -818,7 +821,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
]"
>
<ShareIcon />
{{ formatMessage(messages.share) }}
<span class="bar-label">{{ formatMessage(messages.share) }}</span>
<DropdownIcon />
<template #share-names>
<TextCursorInputIcon />
@@ -849,9 +852,13 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
color-fill="text"
hover-color-fill="background"
>
<button :disabled="ctx.isBusy.value" @click="showBulkDeleteModal">
<button
v-tooltip="formatMessage(commonMessages.deleteLabel)"
:disabled="ctx.isBusy.value"
@click="showBulkDeleteModal"
>
<TrashIcon />
{{ formatMessage(commonMessages.deleteLabel) }}
<span class="bar-label">{{ formatMessage(commonMessages.deleteLabel) }}</span>
</button>
</ButtonStyled>
</template>
@@ -862,6 +869,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:count="pendingDeletionItems.length"
:item-type="ctx.contentTypeLabel.value"
:variant="ctx.deletionContext ?? 'instance'"
:backup-tip="pendingDeletionItems.map((i) => i.project.title).join(', ')"
@delete="confirmDelete"
/>
<ConfirmBulkUpdateModal
@@ -875,6 +883,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
v-if="ctx.unlinkModpack"
ref="confirmUnlinkModal"
:server="ctx.deletionContext === 'server'"
:backup-tip="ctx.modpack.value?.project.title"
@unlink="ctx.unlinkModpack!()"
/>

View File

@@ -51,8 +51,7 @@ export interface ContentManagerContext {
disableAddContent?: Ref<boolean> | ComputedRef<boolean>
disableAddContentTooltip?: string
// Identity & labelling
getItemId: (item: ContentItem) => string
// Labelling
contentTypeLabel: Ref<string> | ComputedRef<string>
// Core actions

View File

@@ -44,9 +44,9 @@ export interface ContentItem extends Omit<
ContentCardTableItem,
'id' | 'projectLink' | 'disabled' | 'overflowOptions'
> {
id: string
file_name: string
file_path?: string
hash?: string
size?: number
project_type: string
has_update: boolean

View File

@@ -679,6 +679,9 @@ const messages = defineMessages({
ref="modpackUpdateModal"
:downgrade="isUpdateDowngrade"
:server="ctx.isServer"
:backup-tip="
[ctx.modpack.value?.title, pendingUpdateVersion?.version_number].filter(Boolean).join(' ')
"
@confirm="handleModpackUpdateConfirm"
@cancel="handleModpackUpdateCancel"
/>
@@ -686,9 +689,15 @@ const messages = defineMessages({
<ConfirmReinstallModal
ref="reinstallModal"
:server="ctx.isServer"
:backup-tip="ctx.modpack.value?.title"
@reinstall="handleReinstall"
/>
<ConfirmUnlinkModal ref="unlinkModal" :server="ctx.isServer" @unlink="handleUnlink" />
<ConfirmUnlinkModal
ref="unlinkModal"
:server="ctx.isServer"
:backup-tip="ctx.modpack.value?.title"
@unlink="handleUnlink"
/>
<ContentDiffModal
v-if="form.pendingPreview.value"

View File

@@ -487,6 +487,7 @@ function addonToContentItem(addon: Archon.Content.v1.Addon): ContentItem {
link: `/${addon.owner.type}/${addon.owner.id}`,
}
: undefined,
id: addon.id,
enabled: !addon.disabled,
file_name: addon.filename,
project_type: addon.kind,
@@ -604,8 +605,8 @@ async function handleBulkUpdate(items: ContentItem[]) {
}
}
async function handleUpdateItem(fileNameKey: string) {
const item = contentItems.value.find((i) => i.file_name === fileNameKey)
async function handleUpdateItem(id: string) {
const item = contentItems.value.find((i) => i.id === id)
if (!item?.has_update || !item.project?.id || !item.version?.id) return
updatingModpack.value = false
@@ -872,7 +873,6 @@ provideContentManager({
})
return filteredReasons.length > 0 ? formatMessage(filteredReasons[0].reason) : null
}),
getItemId: (item) => item.file_path ?? item.file_name,
contentTypeLabel: type,
toggleEnabled: handleToggleEnabled,
deleteItem: handleDeleteItem,
@@ -898,7 +898,7 @@ provideContentManager({
mapToTableItem: (item) => {
const projectType = item.project_type ?? type.value
return {
id: item.file_path ?? item.file_name,
id: item.id,
project: item.project,
projectLink: item.project?.id ? `/${projectType}/${item.project.id}` : undefined,
version: item.version,
@@ -962,6 +962,11 @@ provideContentManager({
<ConfirmModpackUpdateModal
ref="modpackUpdateModal"
:downgrade="isModpackUpdateDowngrade"
:backup-tip="
[modpack?.project.title, pendingModpackUpdateVersion?.version_number]
.filter(Boolean)
.join(' ')
"
server
@confirm="handleModpackUpdateConfirm"
@cancel="handleModpackUpdateCancel"

View File

@@ -374,6 +374,12 @@
"content.page-layout.uploading-files": {
"defaultMessage": "Uploading files ({completed}/{total})"
},
"content.selection-bar.all-already-disabled": {
"defaultMessage": "All selected content is already disabled"
},
"content.selection-bar.all-already-enabled": {
"defaultMessage": "All selected content is already enabled"
},
"content.selection-bar.bulk.deleting": {
"defaultMessage": "Deleting {progress}/{total} {contentType}..."
},
@@ -2151,7 +2157,7 @@
"defaultMessage": "Creating backup"
},
"servers.backups.admonition.fallback-name": {
"defaultMessage": "your backup"
"defaultMessage": "Your backup"
},
"servers.backups.admonition.restore-failed.description": {
"defaultMessage": "Something went wrong while restoring from {backupName}. Please try again or contact support if the issue continues."