fix: app problems post release qa (#5554)

* fix: app problems post release qa

* fix: lint

* fix: dont prefill

* fix: toggle gap

* feat: macs thing

* fix: lint
This commit is contained in:
Calum H.
2026-03-13 20:18:11 +00:00
committed by GitHub
parent 51deba8cd1
commit 86c0937616
31 changed files with 512 additions and 139 deletions

View File

@@ -7,7 +7,7 @@
:disabled="disabled"
class="relative inline-flex shrink-0 rounded-full m-0 transition-all duration-200 cursor-pointer border-none"
:class="[
small ? 'h-5 !w-[38px]' : 'h-8 !w-[52px]',
small ? 'h-5 !w-[40px]' : 'h-8 !w-[60px]',
modelValue ? 'bg-brand' : 'bg-button-bg',
disabled ? 'opacity-50 cursor-not-allowed' : 'btn-wrapper',
]"
@@ -16,11 +16,11 @@
<span
class="absolute rounded-full transition-all duration-200"
:class="[
small ? 'w-4 h-4 top-0.5 left-0.5' : 'w-[18px] h-[18px] top-[7px] left-[7px]',
small ? 'w-4 h-4 top-0.5 left-0.5' : 'w-[24px] h-[24px] top-1 left-1',
modelValue
? small
? 'translate-x-[18px] bg-black/90'
: 'translate-x-5 bg-black/90'
? 'translate-x-5 bg-black/90'
: 'translate-x-7 bg-black/90'
: 'bg-gray',
]"
/>

View File

@@ -22,7 +22,10 @@
<!-- Instance-specific: Name field -->
<div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2">
<span class="font-semibold text-contrast">Name</span>
<StyledInput v-model="ctx.instanceName.value" placeholder="Enter instance name" />
<StyledInput
v-model="ctx.instanceName.value"
:placeholder="ctx.autoInstanceName.value || 'Enter instance name'"
/>
</div>
<!-- Loader chips -->

View File

@@ -3,6 +3,7 @@ import { computed, type ComputedRef, type Ref, ref, type ShallowRef, watch } fro
import type { ComponentExposed } from 'vue-component-type-helpers'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { formatLoaderLabel } from '#ui/utils/loaders'
import { createContext } from '../../../providers'
import type { ImportableLauncher } from '../../../providers/instance-import'
@@ -77,6 +78,7 @@ export interface CreationFlowContextValue {
// Instance-specific state
instanceName: Ref<string>
autoInstanceName: ComputedRef<string>
instanceIcon: Ref<File | null>
instanceIconUrl: Ref<string | null>
instanceIconPath: Ref<string | null>
@@ -121,7 +123,7 @@ export interface CreationFlowContextValue {
onBack: (() => void) | null
// Methods
reset: (instanceCount?: number) => void
reset: (instanceCount?: number) => Promise<void>
setSetupType: (type: SetupType) => void
setImportMode: () => void
browseModpacks: () => void
@@ -138,7 +140,6 @@ export const [injectCreationFlowContext, provideCreationFlowContext] =
// TODO: replace with actual world count from the world list once available
let worldCounter = 0
let instanceCounter = 0
export interface CreationFlowOptions {
availableLoaders?: string[]
@@ -147,6 +148,7 @@ export interface CreationFlowOptions {
isInitialSetup?: boolean
initialLoader?: string
initialGameVersion?: string
fetchExistingInstanceNames?: () => Promise<string[]>
onBack?: () => void
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
@@ -183,6 +185,8 @@ export function createCreationFlowContext(
// Instance-specific state
const instanceName = ref('')
const existingInstanceNames = ref<string[]>([])
const fetchExistingInstanceNames = options.fetchExistingInstanceNames ?? null
const instanceIcon = ref<File | null>(null)
const instanceIconUrl = ref<string | null>(null)
const instanceIconPath = ref<string | null>(null)
@@ -200,6 +204,24 @@ export function createCreationFlowContext(
const selectedLoaderVersion = ref<string | null>(null)
const showSnapshots = ref(false)
const autoInstanceName = computed(() => {
const loader = selectedLoader.value
const version = selectedGameVersion.value
if (!version) return ''
const loaderName = loader ? formatLoaderLabel(loader) : 'Vanilla'
const baseName = `${loaderName} ${version}`
const names = new Set(existingInstanceNames.value)
if (!names.has(baseName)) return baseName
let counter = 1
while (names.has(`${baseName} (${counter})`)) {
counter++
}
return `${baseName} (${counter})`
})
const modpackSelection = ref<ModpackSelection | null>(null)
const modpackFile = ref<File | null>(null)
const modpackFilePath = ref<string | null>(null)
@@ -227,15 +249,14 @@ export function createCreationFlowContext(
() => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla',
)
function reset(instanceCount?: number) {
async function reset() {
if (fetchExistingInstanceNames) {
existingInstanceNames.value = await fetchExistingInstanceNames()
}
setupType.value = null
isImportMode.value = false
worldCounter++
worldName.value = flowType === 'world' ? `World ${worldCounter}` : ''
if (instanceCount != null) {
instanceCounter = instanceCount
}
instanceCounter++
gamemode.value = 'survival'
difficulty.value = 'normal'
worldSeed.value = ''
@@ -245,7 +266,7 @@ export function createCreationFlowContext(
generatorSettingsCustom.value = ''
// Instance-specific
instanceName.value = flowType === 'instance' ? `New instance (${instanceCounter})` : ''
instanceName.value = ''
instanceIconUrl.value = null
instanceIcon.value = null
instanceIconPath.value = null
@@ -356,6 +377,7 @@ export function createCreationFlowContext(
generatorSettingsMode,
generatorSettingsCustom,
instanceName,
autoInstanceName,
instanceIcon,
instanceIconUrl,
instanceIconPath,

View File

@@ -31,6 +31,7 @@ const props = withDefaults(
isInitialSetup?: boolean
initialLoader?: string
initialGameVersion?: string
fetchExistingInstanceNames?: () => Promise<string[]>
onBack?: (() => void) | null
fade?: 'standard' | 'warning' | 'danger'
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
@@ -44,6 +45,7 @@ const props = withDefaults(
isInitialSetup: false,
initialLoader: undefined,
initialGameVersion: undefined,
fetchExistingInstanceNames: undefined,
onBack: null,
},
)
@@ -69,6 +71,7 @@ const ctx = createCreationFlowContext(
isInitialSetup: props.isInitialSetup,
initialLoader: props.initialLoader,
initialGameVersion: props.initialGameVersion,
fetchExistingInstanceNames: props.fetchExistingInstanceNames,
onBack: props.onBack ?? undefined,
searchModpacks: props.searchModpacks,
getProjectVersions: props.getProjectVersions,
@@ -76,8 +79,8 @@ const ctx = createCreationFlowContext(
)
provideCreationFlowContext(ctx)
function show(instanceCount?: number) {
ctx.reset(instanceCount)
async function show() {
await ctx.reset()
modal.value?.setStage(0)
modal.value?.show()
}

View File

@@ -6,7 +6,6 @@ import CustomSetupStage from '../components/CustomSetupStage.vue'
import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context'
function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
if (ctx.flowType === 'instance' && !ctx.instanceName.value?.trim()) return true
if (!ctx.selectedGameVersion.value) return true
if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true
if (

View File

@@ -4,9 +4,11 @@ import {
MoreVerticalIcon,
OrganizationIcon,
SpinnerIcon,
TrashExclamationIcon,
TrashIcon,
TriangleAlertIcon,
} from '@modrinth/assets'
import { useMagicKeys } from '@vueuse/core'
import { Tooltip } from 'floating-vue'
import { computed, getCurrentInstance, ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
@@ -64,7 +66,7 @@ const selected = defineModel<boolean>('selected')
const emit = defineEmits<{
'update:enabled': [value: boolean]
delete: []
delete: [event: MouseEvent]
update: []
}>()
@@ -74,6 +76,9 @@ const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate
const versionNumberRef = ref<HTMLElement | null>(null)
const fileNameRef = ref<HTMLElement | null>(null)
const { shift: shiftHeld } = useMagicKeys()
const deleteHovered = ref(false)
</script>
<template>
@@ -84,9 +89,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
>
<div
class="flex min-w-0 items-center gap-4"
:class="
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none'
"
:class="[
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none',
enabled === false && !disabled ? 'grayscale opacity-50' : '',
]"
>
<Checkbox
v-if="showCheckbox"
@@ -188,7 +194,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
<div
class="hidden flex-col gap-0.5 @[800px]:flex"
:class="hideActions ? 'flex-1' : 'flex-1 min-w-0'"
:class="[
hideActions ? 'flex-1' : 'flex-1 min-w-0',
enabled === false && !disabled ? 'grayscale opacity-50' : '',
]"
>
<template v-if="version">
<AutoLink
@@ -221,7 +230,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
</template>
</div>
<div v-if="!hideActions" class="flex min-w-[160px] shrink-0 items-center justify-end gap-2">
<div
v-if="!hideActions"
class="flex min-w-[160px] shrink-0 items-center justify-end gap-2 transition-colors duration-200"
>
<slot name="additionalButtonsLeft" />
<!-- Fixed width container to reserve space for update button -->
@@ -249,18 +261,34 @@ const fileNameRef = ref<HTMLElement | null>(null)
:model-value="enabled"
:disabled="disabled"
:aria-label="project.title"
small
class="mr-2 my-auto"
@update:model-value="(val) => emit('update:enabled', val as boolean)"
/>
<ButtonStyled v-if="hasDeleteListener && !props.hideDelete" circular type="transparent">
<button
v-tooltip="formatMessage(commonMessages.deleteLabel)"
v-tooltip="
formatMessage(
shiftHeld && deleteHovered
? commonMessages.deleteImmediatelyLabel
: commonMessages.deleteLabel,
)
"
:disabled="disabled"
@click="emit('delete')"
@click="emit('delete', $event)"
@mouseenter="deleteHovered = true"
@mouseleave="deleteHovered = false"
>
<TrashIcon class="size-5 text-secondary" />
<span class="relative size-5">
<TrashIcon
class="absolute inset-0 size-5 text-secondary transition-opacity duration-200"
:class="shiftHeld && deleteHovered ? 'opacity-0' : 'opacity-100'"
/>
<TrashExclamationIcon
class="absolute inset-0 size-5 text-red transition-opacity duration-200"
:class="shiftHeld && deleteHovered ? 'opacity-100' : 'opacity-0'"
/>
</span>
</button>
</ButtonStyled>

View File

@@ -47,7 +47,7 @@ const selectedIds = defineModel<string[]>('selectedIds', { default: () => [] })
const emit = defineEmits<{
'update:enabled': [id: string, value: boolean]
delete: [id: string]
delete: [id: string, event: MouseEvent]
update: [id: string]
sort: [column: ContentCardTableSortColumn, direction: ContentCardTableSortDirection]
}>()
@@ -280,7 +280,11 @@ function handleSort(column: ContentCardTableSortColumn) {
:hide-actions="!hasAnyActions"
:selected="isItemSelected(item.id)"
:class="[
(visibleRange.start + idx) % 2 === 1 ? 'bg-surface-1.5' : 'bg-surface-2',
isItemSelected(item.id)
? 'bg-surface-2.5'
: (visibleRange.start + idx) % 2 === 1
? 'bg-surface-1.5'
: 'bg-surface-2',
'border-0 border-t border-solid border-surface-4',
visibleRange.start + idx === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
]"
@@ -288,7 +292,7 @@ function handleSort(column: ContentCardTableSortColumn) {
(val) => toggleItemSelection(item.id, val ?? false, visibleRange.start + idx)
"
@update:enabled="(val) => emit('update:enabled', item.id, val)"
@delete="emit('delete', item.id)"
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
@update="emit('update', item.id)"
>
<template #additionalButtonsLeft>
@@ -326,13 +330,17 @@ function handleSort(column: ContentCardTableSortColumn) {
:hide-actions="!hasAnyActions"
:selected="isItemSelected(item.id)"
:class="[
index % 2 === 1 ? 'bg-surface-1.5' : 'bg-surface-2',
isItemSelected(item.id)
? 'bg-surface-2.5'
: index % 2 === 1
? 'bg-surface-1.5'
: 'bg-surface-2',
'border-0 border-t border-solid border-surface-4',
index === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
]"
@update:selected="(val) => toggleItemSelection(item.id, val ?? false, index)"
@update:enabled="(val) => emit('update:enabled', item.id, val)"
@delete="emit('delete', item.id)"
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
@update="emit('update', item.id)"
>
<template #additionalButtonsLeft>

View File

@@ -10,7 +10,7 @@
<span class="text-lg font-extrabold text-contrast">{{
header ??
formatMessage(
isModpack ? messages.switchModpackVersionHeader : messages.updateVersionHeader,
isModpack.value ? messages.switchModpackVersionHeader : messages.updateVersionHeader,
)
}}</span>
</template>
@@ -246,10 +246,12 @@ import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
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 { commonMessages } from '#ui/utils/common-messages'
const { formatMessage } = useVIntl()
const debug = useDebugLogger('ContentUpdaterModal')
const messages = defineMessages({
updateVersionHeader: {
@@ -326,8 +328,8 @@ const props = withDefaults(
currentLoader: string
currentVersionId: string
isApp: boolean
/** Whether this is a modpack update (changes header text) */
isModpack?: boolean
/** The project type (e.g. mod, shader, resourcepack, datapack, modpack). */
projectType?: string
projectIconUrl?: string
projectName?: string
header?: string
@@ -337,7 +339,7 @@ const props = withDefaults(
loadingChangelog?: boolean
}>(),
{
isModpack: false,
projectType: undefined,
projectIconUrl: undefined,
projectName: undefined,
header: undefined,
@@ -346,8 +348,10 @@ const props = withDefaults(
},
)
const isModpack = computed(() => props.projectType === 'modpack')
const emit = defineEmits<{
update: [version: Labrinth.Versions.v2.Version]
update: [version: Labrinth.Versions.v2.Version, event: MouseEvent]
cancel: []
/** Emitted when user selects a version, so parent can fetch full version data with changelog */
versionSelect: [version: Labrinth.Versions.v2.Version]
@@ -374,8 +378,20 @@ watch(
// Handle initial selection when versions first arrive
if (newVersions.length > 0 && !selectedVersion.value && pendingInitialVersionId.value) {
const version =
newVersions.find((v) => v.id === pendingInitialVersionId.value) ?? newVersions[0]
const pendingFound = newVersions.find((v) => v.id === pendingInitialVersionId.value)
debug('versions watcher: initial selection', {
pendingInitialVersionId: pendingInitialVersionId.value,
foundPending: !!pendingFound,
currentVersionId: props.currentVersionId,
currentInList: newVersions.some((v) => v.id === props.currentVersionId),
totalVersions: newVersions.length,
loaderDistribution: [...new Set(newVersions.flatMap((v) => v.loaders))],
gameVersionDistribution: [...new Set(newVersions.flatMap((v) => v.game_versions))].slice(
0,
10,
),
})
const version = pendingFound ?? newVersions[0]
selectedVersion.value = version
if (version) {
emit('versionSelect', version)
@@ -386,12 +402,30 @@ watch(
{ deep: true },
)
const NON_MOD_PROJECT_TYPES = new Set(['shader', 'shaderpack', 'resourcepack', 'datapack'])
function isVersionCompatible(version: Labrinth.Versions.v2.Version): boolean {
const hasGameVersion = version.game_versions.includes(props.currentGameVersion)
const hasLoader = version.loaders.some(
(loader) => loader.toLowerCase() === props.currentLoader.toLowerCase(),
)
return hasGameVersion && hasLoader
const skipLoaderCheck = props.projectType != null && NON_MOD_PROJECT_TYPES.has(props.projectType)
const hasLoader =
skipLoaderCheck ||
version.loaders.some((loader) => loader.toLowerCase() === props.currentLoader.toLowerCase())
const compatible = hasGameVersion && hasLoader
if (!compatible) {
debug('isVersionCompatible: INCOMPATIBLE', {
versionId: version.id,
versionNumber: version.version_number,
versionLoaders: version.loaders,
versionGameVersions: version.game_versions,
currentLoader: props.currentLoader,
currentGameVersion: props.currentGameVersion,
projectType: props.projectType,
hasGameVersion,
hasLoader,
skipLoaderCheck,
})
}
return compatible
}
const currentVersion = computed(() => props.versions.find((v) => v.id === props.currentVersionId))
@@ -413,10 +447,19 @@ const filteredVersions = computed(() => {
)
}
const beforeFilterCount = versions.length
if (hideIncompatibleState.value) {
versions = versions.filter(isVersionCompatible)
}
debug('filteredVersions computed', {
totalVersions: props.versions.length,
afterSearchFilter: beforeFilterCount,
afterCompatibilityFilter: versions.length,
hiddenByCompatibility: beforeFilterCount - versions.length,
hideIncompatible: hideIncompatibleState.value,
})
return versions
})
@@ -503,9 +546,9 @@ function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
emit('versionSelect', version)
}
function handleUpdate() {
function handleUpdate(event: MouseEvent) {
if (selectedVersion.value) {
emit('update', selectedVersion.value)
emit('update', selectedVersion.value, event)
hide()
}
}
@@ -519,7 +562,23 @@ function show(initialVersionId?: string) {
searchQuery.value = ''
hideIncompatibleState.value = true
debug('show() called', {
initialVersionId,
currentVersionId: props.currentVersionId,
currentGameVersion: props.currentGameVersion,
currentLoader: props.currentLoader,
projectType: props.projectType,
versionsAvailable: props.versions.length,
})
if (props.versions.length > 0) {
const currentInList = props.versions.find((v) => v.id === props.currentVersionId)
debug('show(): currentVersionId lookup', {
currentVersionId: props.currentVersionId,
foundInList: !!currentInList,
allVersionIds: props.versions.map((v) => v.id),
})
if (initialVersionId) {
selectedVersion.value =
props.versions.find((v) => v.id === initialVersionId) ?? props.versions[0]
@@ -533,6 +592,9 @@ function show(initialVersionId?: string) {
} else {
selectedVersion.value = null
pendingInitialVersionId.value = initialVersionId
debug('show(): no versions yet, deferring selection', {
pendingInitialVersionId: initialVersionId,
})
}
modal.value?.show()

View File

@@ -43,6 +43,9 @@
class="size-5 shrink-0 text-brand-orange hover:brightness-110"
/>
</div>
<span class="text-secondary">
{{ formatMessage(messages.shiftClickHint) }}
</span>
</div>
</template>
@@ -111,5 +114,9 @@ const messages = defineMessages({
defaultMessage:
"A backup is in progress, it's recommended to wait for it to finish before performing this action.",
},
shiftClickHint: {
id: 'content.inline-backup.shift-click-hint',
defaultMessage: 'Hold Shift while clicking to skip confirmation.',
},
})
</script>

View File

@@ -1,6 +1,9 @@
<script setup lang="ts">
import {
ArrowUpDownIcon,
ArrowDownAZIcon,
ArrowDownZAIcon,
ClockArrowDownIcon,
ClockArrowUpIcon,
CodeIcon,
CompassIcon,
DownloadIcon,
@@ -77,9 +80,13 @@ const messages = defineMessages({
id: 'content.page-layout.sort.alphabetical',
defaultMessage: 'Alphabetical',
},
sortDateAdded: {
id: 'content.page-layout.sort.date-added',
defaultMessage: 'Date added',
sortDateAddedNewest: {
id: 'content.page-layout.sort.date-added-newest',
defaultMessage: 'Newest first',
},
sortDateAddedOldest: {
id: 'content.page-layout.sort.date-added-oldest',
defaultMessage: 'Oldest first',
},
updateAll: {
id: 'content.page-layout.update-all',
@@ -147,34 +154,55 @@ const uploadOverallProgress = computed(() => {
return Math.min((state.completedFiles + state.currentFileProgress) / state.totalFiles, 1)
})
type SortMode = 'alphabetical' | 'date-added'
const sortMode = ref<SortMode>('alphabetical')
type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' | 'date-added-oldest'
const sortMode = ref<SortMode>('alphabetical-asc')
const sortLabels: Record<SortMode, () => string> = {
alphabetical: () => formatMessage(messages.sortAlphabetical),
'date-added': () => formatMessage(messages.sortDateAdded),
'alphabetical-asc': () => formatMessage(messages.sortAlphabetical),
'alphabetical-desc': () => formatMessage(messages.sortAlphabetical),
'date-added-newest': () => formatMessage(messages.sortDateAddedNewest),
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
}
function cycleSortMode() {
const modes: SortMode[] = ['alphabetical', 'date-added']
const modes: SortMode[] = [
'alphabetical-asc',
'date-added-newest',
'alphabetical-desc',
'date-added-oldest',
]
const idx = modes.indexOf(sortMode.value)
sortMode.value = modes[(idx + 1) % modes.length]
}
const sortedItems = computed(() => {
const items = [...ctx.items.value]
if (sortMode.value === 'date-added') {
return items.sort((a, b) => {
const dateA = a.date_added ?? ''
const dateB = b.date_added ?? ''
return dateB.localeCompare(dateA)
})
switch (sortMode.value) {
case 'alphabetical-desc':
return items.sort((a, b) => {
const nameA = a.project?.title ?? a.file_name
const nameB = b.project?.title ?? b.file_name
return nameB.toLowerCase().localeCompare(nameA.toLowerCase())
})
case 'date-added-newest':
return items.sort((a, b) => {
const dateA = a.date_added ?? ''
const dateB = b.date_added ?? ''
return dateB.localeCompare(dateA)
})
case 'date-added-oldest':
return items.sort((a, b) => {
const dateA = a.date_added ?? ''
const dateB = b.date_added ?? ''
return dateA.localeCompare(dateB)
})
default:
return items.sort((a, b) => {
const nameA = a.project?.title ?? a.file_name
const nameB = b.project?.title ?? b.file_name
return nameA.toLowerCase().localeCompare(nameB.toLowerCase())
})
}
return items.sort((a, b) => {
const nameA = a.project?.title ?? a.file_name
const nameB = b.project?.title ?? b.file_name
return nameA.toLowerCase().localeCompare(nameB.toLowerCase())
})
})
const { searchQuery, search } = useContentSearch(sortedItems, [
@@ -223,20 +251,28 @@ async function handleRefresh() {
}
}
const filteredItems = computed(() => applyFilters(search(sortedItems.value)))
const tableItems = computed<ContentCardTableItem[]>(() =>
filteredItems.value.map((item) => {
const filteredItems = computed(() => {
const sorted = sortedItems.value
const searched = search(sorted)
return applyFilters(searched)
})
const tableItems = computed<ContentCardTableItem[]>(() => {
return filteredItems.value.map((item) => {
const base = ctx.mapToTableItem(item)
return {
...base,
disabled: isChanging(base.id) || ctx.isBusy.value || item.installing === true,
disabled:
isChanging(base.id) ||
ctx.isBusy.value ||
isBulkOperating.value ||
item.installing === true,
installing: item.installing === true,
hasUpdate: !ctx.isPackLocked.value && item.has_update,
isClientOnly: isClientOnlyEnvironment(item.environment),
overflowOptions: ctx.getOverflowOptions?.(item),
}
}),
)
})
})
const hasOutdatedProjects = computed(() => ctx.items.value.some((p) => p.has_update))
@@ -244,17 +280,25 @@ const hasOutdatedProjects = computed(() => ctx.items.value.some((p) => p.has_upd
const pendingDeletionItems = ref<ContentItem[]>([])
const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>()
function handleDeleteById(id: string) {
function handleDeleteById(id: string, event?: MouseEvent) {
const item = ctx.items.value.find((i) => ctx.getItemId(i) === id)
if (item) {
pendingDeletionItems.value = [item]
confirmDeletionModal.value?.show()
if (event?.shiftKey) {
confirmDelete()
} else {
confirmDeletionModal.value?.show()
}
}
}
function showBulkDeleteModal() {
function showBulkDeleteModal(event?: MouseEvent) {
pendingDeletionItems.value = [...selectedItems.value]
confirmDeletionModal.value?.show()
if (event?.shiftKey) {
confirmDelete()
} else {
confirmDeletionModal.value?.show()
}
}
async function confirmDelete() {
@@ -359,20 +403,28 @@ const pendingBulkUpdateItems = ref<ContentItem[]>([])
const hasBulkUpdateSupport = computed(() => !!(ctx.bulkUpdateItem || ctx.bulkUpdateItems))
function promptUpdateAll() {
function promptUpdateAll(event?: MouseEvent) {
if (!hasBulkUpdateSupport.value) return
const items = ctx.items.value.filter((item) => item.has_update)
if (items.length === 0) return
pendingBulkUpdateItems.value = items
confirmBulkUpdateModal.value?.show()
if (event?.shiftKey) {
confirmBulkUpdate()
} else {
confirmBulkUpdateModal.value?.show()
}
}
function promptUpdateSelected() {
function promptUpdateSelected(event?: MouseEvent) {
if (!hasBulkUpdateSupport.value) return
const items = selectedItems.value.filter((item) => item.has_update)
if (items.length === 0) return
pendingBulkUpdateItems.value = items
confirmBulkUpdateModal.value?.show()
if (event?.shiftKey) {
confirmBulkUpdate()
} else {
confirmBulkUpdateModal.value?.show()
}
}
async function confirmBulkUpdate() {
@@ -505,8 +557,8 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
clearable
:placeholder="
formatMessage(messages.searchPlaceholder, {
count: ctx.items.value.length,
contentType: `${ctx.contentTypeLabel.value}${ctx.items.value.length === 1 ? '' : 's'}`,
count: tableItems.length,
contentType: `${ctx.contentTypeLabel.value}${tableItems.length === 1 ? '' : 's'}`,
})
"
/>
@@ -580,7 +632,11 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
"
@click="cycleSortMode"
>
<ArrowUpDownIcon />
<ArrowDownZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
v-else-if="sortMode === 'date-added-newest'"
/><ClockArrowUpIcon
v-else-if="sortMode === 'date-added-oldest'"
/><ArrowDownAZIcon v-else />
{{ sortLabels[sortMode]() }}
</button>
</ButtonStyled>
@@ -596,7 +652,11 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
"
@click="cycleSortMode"
>
<ArrowUpDownIcon />
<ArrowDownZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
v-else-if="sortMode === 'date-added-newest'"
/><ClockArrowUpIcon
v-else-if="sortMode === 'date-added-oldest'"
/><ArrowDownAZIcon v-else />
{{ sortLabels[sortMode]() }}
</button>
</ButtonStyled>

View File

@@ -88,14 +88,18 @@ const disabledPlatforms = computed(() => {
const showModpackVersionActions = ctx.showModpackVersionActions ?? true
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version) {
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event?: MouseEvent) {
pendingUpdateVersion.value = version
const currentVersionId = ctx.updaterModalProps.value.currentVersionId
const currentVersion = form.updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isUpdateDowngrade.value = currentVersion
? new Date(version.date_published) < new Date(currentVersion.date_published)
: false
modpackUpdateModal.value?.show()
if (event?.shiftKey) {
handleModpackUpdateConfirm()
} else {
modpackUpdateModal.value?.show()
}
}
function handleModpackUpdateConfirm() {
@@ -363,7 +367,7 @@ const messages = defineMessages({
<button
class="!shadow-none"
:disabled="ctx.isBusy.value"
@click="unlinkModal?.show()"
@click="(e: MouseEvent) => (e.shiftKey ? handleUnlink() : unlinkModal?.show())"
>
<UnlinkIcon class="size-5" />
{{
@@ -395,7 +399,9 @@ const messages = defineMessages({
<button
class="!shadow-none"
:disabled="ctx.isBusy.value"
@click="reinstallModal?.show()"
@click="
(e: MouseEvent) => (e.shiftKey ? handleReinstall() : reinstallModal?.show())
"
>
<SpinnerIcon v-if="ctx.reinstalling?.value" class="animate-spin" />
<DownloadIcon v-else class="size-5" />
@@ -659,7 +665,7 @@ const messages = defineMessages({
:current-loader="ctx.updaterModalProps.value.currentLoader"
:current-version-id="ctx.updaterModalProps.value.currentVersionId"
:is-app="ctx.isApp"
:is-modpack="true"
project-type="modpack"
:project-icon-url="ctx.updaterModalProps.value.projectIconUrl"
:project-name="ctx.updaterModalProps.value.projectName"
:loading="form.loadingVersions.value"

View File

@@ -714,15 +714,20 @@ function resetUpdateState() {
loadingChangelog.value = false
}
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version) {
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?: MouseEvent) {
if (updatingModpack.value) {
const currentVersionId = contentQuery.data.value?.modpack?.spec.version_id
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isModpackUpdateDowngrade.value = currentVersion
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
: false
pendingModpackUpdateVersion.value = selectedVersion
modpackUpdateModal.value?.show()
if (event?.shiftKey) {
pendingModpackUpdateVersion.value = selectedVersion
handleModpackUpdateConfirm()
} else {
const currentVersionId = contentQuery.data.value?.modpack?.spec.version_id
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isModpackUpdateDowngrade.value = currentVersion
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
: false
pendingModpackUpdateVersion.value = selectedVersion
modpackUpdateModal.value?.show()
}
return
}
@@ -860,7 +865,7 @@ provideContentManager({
: (updatingProject?.version?.id ?? '')
"
:is-app="false"
:is-modpack="updatingModpack"
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
:project-icon-url="
updatingModpack ? modpack?.project.icon_url : updatingProject?.project?.icon_url
"

View File

@@ -287,6 +287,9 @@
"content.inline-backup.create-backup": {
"defaultMessage": "Create backup"
},
"content.inline-backup.shift-click-hint": {
"defaultMessage": "Hold Shift while clicking to skip confirmation."
},
"content.inline-backup.warning-body": {
"defaultMessage": "We recommend creating a backup before proceeding so you can restore your {type, select, server {world} other {instance}} if anything breaks."
},
@@ -353,8 +356,11 @@
"content.page-layout.sort.alphabetical": {
"defaultMessage": "Alphabetical"
},
"content.page-layout.sort.date-added": {
"defaultMessage": "Date added"
"content.page-layout.sort.date-added-newest": {
"defaultMessage": "Newest first"
},
"content.page-layout.sort.date-added-oldest": {
"defaultMessage": "Oldest first"
},
"content.page-layout.sort.label": {
"defaultMessage": "Sort by {mode}"
@@ -875,6 +881,9 @@
"label.delete": {
"defaultMessage": "Delete"
},
"label.delete-immediately": {
"defaultMessage": "Delete immediately"
},
"label.description": {
"defaultMessage": "Description"
},

View File

@@ -85,6 +85,10 @@ export const commonMessages = defineMessages({
id: 'label.delete',
defaultMessage: 'Delete',
},
deleteImmediatelyLabel: {
id: 'label.delete-immediately',
defaultMessage: 'Delete immediately',
},
descriptionLabel: {
id: 'label.description',
defaultMessage: 'Description',