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

@@ -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