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

@@ -68,11 +68,11 @@ import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
@@ -151,6 +151,9 @@ const {
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
} = setupProviders(notificationManager)
const news = ref([])
@@ -424,7 +427,9 @@ const {
handleNavigate: handleContentInstallNavigate,
handleCancel: handleContentInstallCancel,
setContentInstallModal,
setInstallConfirmModal: setContentInstallConfirmModal,
setModpackAlreadyInstalledModal: setContentInstallModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance,
setIncompatibilityWarningModal: setContentIncompatibilityWarningModal,
} = contentInstall
@@ -438,8 +443,9 @@ const {
} = serverInstall
const modInstallModal = ref()
const modpackAlreadyInstalledModal = ref()
const contentInstallModpackAlreadyInstalledModal = ref()
const addServerToInstanceModal = ref()
const installConfirmModal = ref()
const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const updateToPlayModal = ref()
@@ -519,8 +525,9 @@ onMounted(() => {
error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value)
setContentIncompatibilityWarningModal(incompatibilityWarningModal.value)
setContentInstallConfirmModal(installConfirmModal.value)
setContentInstallModal(modInstallModal.value)
setContentInstallModpackAlreadyInstalledModal(contentInstallModpackAlreadyInstalledModal.value)
setModpackAlreadyInstalledModal(modpackAlreadyInstalledModal.value)
setServerAddServerToInstanceModal(addServerToInstanceModal.value)
setServerInstallToPlayModal(installToPlayModal.value)
setServerUpdateToPlayModal(updateToPlayModal.value)
@@ -1295,9 +1302,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@navigate="handleContentInstallNavigate"
@cancel="handleContentInstallCancel"
/>
<ModpackAlreadyInstalledModal
ref="modpackAlreadyInstalledModal"
@create-anyway="handleModpackDuplicateCreateAnyway"
@go-to-instance="handleModpackDuplicateGoToInstance"
/>
<AddServerToInstanceModal ref="addServerToInstanceModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<InstallConfirmModal ref="installConfirmModal" />
<ModpackAlreadyInstalledModal
ref="contentInstallModpackAlreadyInstalledModal"
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@go-to-instance="handleContentInstallModpackDuplicateGoToInstance"
/>
<InstallToPlayModal ref="installToPlayModal" />
<UpdateToPlayModal ref="updateToPlayModal" />
</template>

View File

@@ -1,77 +0,0 @@
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { create_profile_and_install as pack_install } from '@/helpers/pack'
const { handleError } = injectNotificationManager()
const versionId = ref()
const project = ref()
const confirmModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
const onCreateInstance = ref(() => {})
defineExpose({
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
project.value = projectVal
versionId.value = versionIdVal
installing.value = false
confirmModal.value.show()
onInstall.value = callback
onCreateInstance.value = createInstanceCallback
trackEvent('PackInstallStart')
},
})
async function install() {
installing.value = true
confirmModal.value.hide()
await pack_install(
project.value.id,
versionId.value,
project.value.title,
project.value.icon_url,
onCreateInstance.value,
).catch(handleError)
trackEvent('PackInstall', {
id: project.value.id,
version_id: versionId.value,
title: project.value.title,
source: 'ConfirmModal',
})
onInstall.value(versionId.value)
installing.value = false
}
</script>
<template>
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
<div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
<Button color="primary" :disabled="installing" @click="install()"
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
>
</div>
</div>
</ModalWrapper>
</template>
<style lang="scss" scoped>
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.header)" fade="warning" max-width="500px">
<Admonition type="warning" :header="formatMessage(messages.admonitionHeader)">
{{ formatMessage(messages.admonitionBody, { instanceName }) }}
</Admonition>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button class="!border !border-surface-4" @click="handleGoToInstance">
<ExternalIcon />
{{ formatMessage(messages.goToInstance) }}
</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="handleCreateAnyway">
<PlusIcon />
{{ formatMessage(messages.createAnyway) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import { ExternalIcon, PlusIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
import { ref } from 'vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
header: {
id: 'app.instance.modpack-already-installed.header',
defaultMessage: 'Modpack already installed',
},
admonitionHeader: {
id: 'app.instance.modpack-already-installed.admonition-header',
defaultMessage: 'Duplicate modpack',
},
admonitionBody: {
id: 'app.instance.modpack-already-installed.admonition-body',
defaultMessage: 'This modpack is already installed in the "{instanceName}" instance.',
},
goToInstance: {
id: 'app.instance.modpack-already-installed.go-to-instance',
defaultMessage: 'Go to instance',
},
createAnyway: {
id: 'app.instance.modpack-already-installed.create-anyway',
defaultMessage: 'Create anyway',
},
})
const emit = defineEmits<{
(e: 'go-to-instance', instancePath: string): void
(e: 'create-anyway'): void
}>()
const modal = ref<InstanceType<typeof NewModal>>()
const instanceName = ref('')
const instancePath = ref('')
function show(name: string, path: string) {
instanceName.value = name
instancePath.value = path
modal.value?.show()
}
function handleGoToInstance() {
modal.value?.hide()
emit('go-to-instance', instancePath.value)
}
function handleCreateAnyway() {
modal.value?.hide()
emit('create-anyway')
}
defineExpose({
show,
})
</script>

View File

@@ -1,6 +1,6 @@
import type { ModrinthId } from '@modrinth/utils'
type GameInstance = {
export type GameInstance = {
path: string
install_stage: InstallStage
@@ -46,7 +46,7 @@ type LinkedData = {
locked: boolean
}
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
metadata?: {

View File

@@ -41,6 +41,21 @@
"app.instance.confirm-delete.header": {
"message": "Delete instance"
},
"app.instance.modpack-already-installed.admonition-body": {
"message": "This modpack is already installed in the \"{instanceName}\" instance."
},
"app.instance.modpack-already-installed.admonition-header": {
"message": "Duplicate modpack"
},
"app.instance.modpack-already-installed.create-anyway": {
"message": "Create anyway"
},
"app.instance.modpack-already-installed.go-to-instance": {
"message": "Go to instance"
},
"app.instance.modpack-already-installed.header": {
"message": "Modpack already installed"
},
"app.instance.mods.content-type-project": {
"message": "project"
},

View File

@@ -20,6 +20,11 @@
<ConfirmModpackUpdateModal
ref="modpackUpdateConfirmModal"
:downgrade="isModpackUpdateDowngrade"
:backup-tip="
[linkedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
.filter(Boolean)
.join(' ')
"
@confirm="handleModpackUpdateConfirm"
@cancel="handleModpackUpdateCancel"
/>
@@ -471,7 +476,7 @@ async function handleModpackContentToggle(item: ContentItem) {
}
async function handleModpackContentBulkToggle(items: ContentItem[]) {
await Promise.all(items.map((item) => toggleDisableMod(item)))
await Promise.all(items.map((item) => _toggleDisableMod(item)))
}
async function handleModpackContent() {
@@ -814,13 +819,12 @@ provideContentManager({
isPackLocked,
isBusy: isInstanceBusy,
isBulkOperating,
getItemId: (item) => item.file_path ?? item.file_name,
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
toggleEnabled: toggleDisableMod,
bulkEnableItems: (items) =>
Promise.all(items.map((item) => toggleDisableMod(item))).then(() => {}),
Promise.all(items.map((item) => _toggleDisableMod(item))).then(() => {}),
bulkDisableItems: (items) =>
Promise.all(items.map((item) => toggleDisableMod(item))).then(() => {}),
Promise.all(items.map((item) => _toggleDisableMod(item))).then(() => {}),
deleteItem: removeMod,
bulkDeleteItems: (items) => Promise.all(items.map((item) => removeMod(item))).then(() => {}),
refresh: () => initProjects('must_revalidate'),
@@ -838,7 +842,7 @@ provideContentManager({
dismissContentHint,
shareItems: handleShareItems,
mapToTableItem: (item) => ({
id: item.file_path ?? item.file_name,
id: item.id,
project: item.project ?? {
id: item.file_name,
slug: null,

View File

@@ -26,6 +26,7 @@ import {
remove_project,
} from '@/helpers/profile.js'
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance, InstanceLoader } from '@/helpers/types'
import {
findPreferredVersion,
installVersionDependencies,
@@ -37,13 +38,8 @@ interface ModalRef {
hide: () => void
}
interface InstallConfirmModalRef {
show: (
project: Labrinth.Projects.v2.Project,
version: string,
callback: (versionId?: string) => void,
createInstanceCallback: (profile: string) => void,
) => void
interface ModpackAlreadyInstalledModalRef {
show: (instanceName: string, instancePath: string) => void
}
interface IncompatibilityWarningModalRef {
@@ -92,7 +88,9 @@ export interface ContentInstallContext {
handleNavigate: (instance: ContentInstallInstance) => void
handleCancel: () => void
setContentInstallModal: (ref: ModalRef) => void
setInstallConfirmModal: (ref: InstallConfirmModalRef) => void
setModpackAlreadyInstalledModal: (ref: ModpackAlreadyInstalledModalRef) => void
handleModpackDuplicateCreateAnyway: () => Promise<void>
handleModpackDuplicateGoToInstance: (instancePath: string) => void
setIncompatibilityWarningModal: (ref: IncompatibilityWarningModalRef) => void
install: (
projectId: string,
@@ -140,12 +138,13 @@ export function createContentInstall(opts: {
) {
const primaryFile = version?.files?.find((f) => f.primary) ?? version?.files?.[0]
const placeholder: ContentItem = {
id: `__installing_${project.id}`,
file_name: `__installing_${project.id}`,
project: {
id: project.id,
slug: project.slug ?? null,
slug: project.slug ?? '',
title: project.title,
icon_url: project.icon_url ?? null,
icon_url: project.icon_url ?? undefined,
},
version: version
? {
@@ -183,18 +182,26 @@ export function createContentInstall(opts: {
}
let modalRef: ModalRef | null = null
let installConfirmModalRef: InstallConfirmModalRef | null = null
let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null
let incompatibilityWarningModalRef: IncompatibilityWarningModalRef | null = null
let currentProject: Labrinth.Projects.v2.Project | null = null
let currentVersions: Labrinth.Versions.v2.Version[] = []
let currentCallback: (versionId?: string) => void = () => {}
let profileMap: Record<string, GameInstance> = {}
let pendingModpackInstall: {
project: Labrinth.Projects.v2.Project
version: string
source: string
callback: (versionId?: string) => void
createInstanceCallback: (profile: string) => void
} | null = null
async function showModInstallModal(
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
onInstall: (versionId?: string) => void,
hints?: { preferredLoader?: string; preferredGameVersion?: string },
hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
) {
currentProject = project
currentVersions = versions
@@ -379,10 +386,10 @@ export function createContentInstall(opts: {
trackEvent('ProjectInstall', {
loader: profile.loader,
game_version: profile.game_version,
id: currentProject.id,
id: currentProject!.id,
version_id: version.id,
project_type: currentProject.project_type,
title: currentProject.title,
project_type: currentProject!.project_type,
title: currentProject!.title,
source: 'ProjectInstallModal',
})
currentCallback(version.id)
@@ -433,10 +440,10 @@ export function createContentInstall(opts: {
trackEvent('ProjectInstall', {
loader: data.loader,
game_version: data.gameVersion,
id: currentProject.id,
id: currentProject!.id,
version_id: version.id,
project_type: currentProject.project_type,
title: currentProject.title,
project_type: currentProject!.project_type,
title: currentProject!.title,
source: 'ProjectInstallModal',
})
@@ -470,28 +477,28 @@ export function createContentInstall(opts: {
if (project.project_type === 'modpack') {
const version = versionId ?? project.versions[project.versions.length - 1]
const packs = await list()
const existingPack = packs.find((pack) => pack.linked_data?.project_id === project.id)
if (
packs.length === 0 ||
!packs.find((pack) => pack.linked_data?.project_id === project.id)
) {
await packInstall(
project.id,
version,
project.title,
project.icon_url,
createInstanceCallback,
)
trackEvent('PackInstall', {
id: project.id,
version_id: version,
title: project.title,
source,
})
callback(version)
} else {
installConfirmModalRef?.show(project, version, callback, createInstanceCallback)
if (existingPack) {
pendingModpackInstall = { project, version, source, callback, createInstanceCallback }
modpackAlreadyInstalledModalRef?.show(existingPack.name, existingPack.path)
return
}
await packInstall(
project.id,
version,
project.title,
project.icon_url,
createInstanceCallback,
)
trackEvent('PackInstall', {
id: project.id,
version_id: version,
title: project.title,
source,
})
callback(version)
} else if (instancePath) {
const [instanceOrNull, instanceProjects, versions] = await Promise.all([
get(instancePath),
@@ -577,8 +584,31 @@ export function createContentInstall(opts: {
setContentInstallModal(ref: ModalRef) {
modalRef = ref
},
setInstallConfirmModal(ref: InstallConfirmModalRef) {
installConfirmModalRef = ref
setModpackAlreadyInstalledModal(ref: ModpackAlreadyInstalledModalRef) {
modpackAlreadyInstalledModalRef = ref
},
async handleModpackDuplicateCreateAnyway() {
if (!pendingModpackInstall) return
const { project, version, source, callback, createInstanceCallback } = pendingModpackInstall
pendingModpackInstall = null
await packInstall(
project.id,
version,
project.title,
project.icon_url,
createInstanceCallback,
)
trackEvent('PackInstall', {
id: project.id,
version_id: version,
title: project.title,
source,
})
callback(version)
},
handleModpackDuplicateGoToInstance(instancePath: string) {
pendingModpackInstall = null
opts.router.push(`/instance/${encodeURIComponent(instancePath)}/`)
},
setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) {
incompatibilityWarningModalRef = ref

View File

@@ -1,18 +1,33 @@
import type { AbstractWebNotificationManager, CreationFlowContextValue } from '@modrinth/ui'
import { provide, useTemplateRef } from 'vue'
import type {
AbstractWebNotificationManager,
CreationFlowContextValue,
CreationFlowModal,
} from '@modrinth/ui'
import { provide, ref, useTemplateRef } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
import { useRouter } from 'vue-router'
import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
import { import_instance } from '@/helpers/import.js'
import { create_profile_and_install, create_profile_and_install_from_file } from '@/helpers/pack'
import { create, list } from '@/helpers/profile.js'
import type { InstanceLoader } from '@/helpers/types'
export function setupCreationModal(notificationManager: AbstractWebNotificationManager) {
const { handleError } = notificationManager
const router = useRouter()
const installationModal = useTemplateRef('installationModal')
const installationModal =
useTemplateRef<ComponentExposed<typeof CreationFlowModal>>('installationModal')
const modpackAlreadyInstalledModal = ref<InstanceType<typeof ModpackAlreadyInstalledModal>>()
function setModpackAlreadyInstalledModal(
modal: InstanceType<typeof ModpackAlreadyInstalledModal>,
) {
modpackAlreadyInstalledModal.value = modal
}
async function fetchExistingInstanceNames(): Promise<string[]> {
const instances = await list().catch(handleError)
@@ -23,10 +38,34 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
installationModal.value?.show()
})
async function handleCreate(config: CreationFlowContextValue) {
installationModal.value?.hide()
async function proceedWithModpackCreation(
projectId: string,
versionId: string,
name: string,
iconUrl?: string,
) {
await create_profile_and_install(projectId, versionId, name, iconUrl).catch(handleError)
trackEvent('InstanceCreate', { source: 'CreationModalModpack' })
}
async function handleCreate(config: CreationFlowContextValue) {
try {
if (config.modpackSelection.value) {
const { projectId, versionId, name, iconUrl } = config.modpackSelection.value
const instances = await list().catch(handleError)
const existingInstance = instances?.find((i) => i.linked_data?.project_id === projectId)
if (existingInstance) {
pendingModpackCreation.value = { projectId, versionId, name, iconUrl }
installationModal.value?.hide()
modpackAlreadyInstalledModal.value?.show(existingInstance.name, existingInstance.path)
return
}
}
installationModal.value?.hide()
if (config.isImportMode.value) {
for (const [launcherName, instanceSet] of Object.entries(
config.importSelectedInstances.value,
@@ -43,8 +82,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
if (config.modpackSelection.value) {
const { projectId, versionId, name, iconUrl } = config.modpackSelection.value
await create_profile_and_install(projectId, versionId, name, iconUrl).catch(handleError)
trackEvent('InstanceCreate', { source: 'CreationModalModpack' })
await proceedWithModpackCreation(projectId, versionId, name, iconUrl)
return
}
@@ -66,26 +104,40 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
await create(
name,
config.selectedGameVersion.value,
loader,
config.selectedGameVersion.value!,
loader as InstanceLoader,
loaderVersion,
iconPath,
false,
).catch(handleError)
trackEvent('InstanceCreate', {
profile_name: name,
game_version: config.selectedGameVersion.value,
loader,
loader_version: loaderVersion,
has_icon: !!iconPath,
source: 'CreationModal',
})
} catch (err) {
handleError(err)
handleError(err as Error)
}
}
const pendingModpackCreation = ref<{
projectId: string
versionId: string
name: string
iconUrl?: string
} | null>(null)
async function handleModpackDuplicateCreateAnyway() {
if (!pendingModpackCreation.value) return
const { projectId, versionId, name, iconUrl } = pendingModpackCreation.value
pendingModpackCreation.value = null
await proceedWithModpackCreation(projectId, versionId, name, iconUrl)
}
function handleModpackDuplicateGoToInstance(instancePath: string) {
pendingModpackCreation.value = null
router.push(`/instance/${encodeURIComponent(instancePath)}/`)
}
function handleBrowseModpacks() {
installationModal.value?.hide()
router.push('/browse/modpack')
@@ -113,5 +165,8 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
}
}

View File

@@ -75,6 +75,7 @@
"semver": "^7.5.4",
"three": "^0.172.0",
"vue-confetti-explosion": "^1.0.2",
"vue-router": "*",
"vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.5.2",

View File

@@ -35,8 +35,9 @@ pub struct ContentItem {
pub file_name: String,
/// Relative path to the file within the profile
pub file_path: String,
/// SHA1 hash of the file
pub hash: String,
/// Stable frontend identifier (SHA1 hash of file content, survives renames).
/// Not a project or version ID.
pub id: String,
/// File size in bytes
pub size: u64,
/// Whether the file is enabled (not .disabled)
@@ -542,7 +543,7 @@ async fn profile_files_to_content_items(
ContentItem {
file_name: file.file_name.clone(),
file_path: path.clone(),
hash: file.hash.clone(),
id: file.hash.clone(),
size: file.size,
enabled: !file.file_name.ends_with(".disabled"),
project_type: file.project_type,
@@ -726,7 +727,7 @@ pub async fn dependencies_to_content_items(
)
}),
file_path: String::new(),
hash: String::new(),
id: String::new(),
size: version
.and_then(|v| v.files.first())
.map(|f| f.size as u64)

View File

@@ -19,6 +19,7 @@ import _ArrowLeftRightIcon from './icons/arrow-left-right.svg?component'
import _ArrowUpIcon from './icons/arrow-up.svg?component'
import _ArrowUpDownIcon from './icons/arrow-up-down.svg?component'
import _ArrowUpRightIcon from './icons/arrow-up-right.svg?component'
import _ArrowUpZAIcon from './icons/arrow-up-z-a.svg?component'
import _AsteriskIcon from './icons/asterisk.svg?component'
import _BadgeCheckIcon from './icons/badge-check.svg?component'
import _BadgeDollarSignIcon from './icons/badge-dollar-sign.svg?component'
@@ -404,6 +405,7 @@ export const ArrowLeftRightIcon = _ArrowLeftRightIcon
export const ArrowUpIcon = _ArrowUpIcon
export const ArrowUpDownIcon = _ArrowUpDownIcon
export const ArrowUpRightIcon = _ArrowUpRightIcon
export const ArrowUpZAIcon = _ArrowUpZAIcon
export const AsteriskIcon = _AsteriskIcon
export const BadgeCheckIcon = _BadgeCheckIcon
export const BadgeDollarSignIcon = _BadgeDollarSignIcon

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-arrow-up-z-a"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m3 8 4-4 4 4" />
<path d="M7 4v16" />
<path d="M15 4h5l-5 6h5" />
<path d="M15 20v-3.5a2.5 2.5 0 0 1 5 0V20" />
<path d="M20 18h-5" />
</svg>

After

Width:  |  Height:  |  Size: 446 B

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."

13
pnpm-lock.yaml generated
View File

@@ -350,6 +350,9 @@ importers:
vue-confetti-explosion:
specifier: ^1.0.2
version: 1.0.2(vue@3.5.27(typescript@5.9.3))
vue-router:
specifier: '*'
version: 4.6.4(vue@3.5.27(typescript@5.9.3))
vue-typed-virtual-list:
specifier: ^1.0.10
version: 1.0.10(vue@3.5.27(typescript@5.9.3))
@@ -8689,6 +8692,7 @@ packages:
tar@7.5.7:
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
terser@5.46.0:
resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==}
@@ -8975,6 +8979,7 @@ packages:
unplugin-vue-router@0.19.2:
resolution: {integrity: sha512-u5dgLBarxE5cyDK/hzJGfpCTLIAyiTXGlo85COuD4Nssj6G7NxS+i9mhCWz/1p/ud1eMwdcUbTXehQe41jYZUA==}
deprecated: 'Merged into vuejs/router. Migrate: https://router.vuejs.org/guide/migration/v4-to-v5.html'
peerDependencies:
'@vue/compiler-sfc': ^3.5.17
vue-router: ^4.6.0
@@ -9406,8 +9411,8 @@ packages:
vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
vue-component-type-helpers@3.2.5:
resolution: {integrity: sha512-tkvNr+bU8+xD/onAThIe7CHFvOJ/BO6XCOrxMzeytJq40nTfpGDJuVjyCM8ccGZKfAbGk2YfuZyDMXM56qheZQ==}
vue-component-type-helpers@3.2.6:
resolution: {integrity: sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==}
vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
@@ -12755,7 +12760,7 @@ snapshots:
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
type-fest: 2.19.0
vue: 3.5.27(typescript@5.9.3)
vue-component-type-helpers: 3.2.5
vue-component-type-helpers: 3.2.6
'@stripe/stripe-js@7.9.0': {}
@@ -19524,7 +19529,7 @@ snapshots:
vue-component-type-helpers@3.2.4: {}
vue-component-type-helpers@3.2.5: {}
vue-component-type-helpers@3.2.6: {}
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
dependencies: