Files
Modrinth-plus/packages/ui/src/layouts/shared/content-tab/components/ContentSelectionBar.vue
Calum H. 1d10af09f5 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
2026-03-18 18:03:55 +00:00

233 lines
6.7 KiB
Vue

<script setup lang="ts">
import { PowerIcon, PowerOffIcon, XIcon } from '@modrinth/assets'
import { computed } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import FloatingActionBar from '#ui/components/base/FloatingActionBar.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
import type { BulkOperationType } from '../composables/bulk-operations'
import type { ContentItem } from '../types'
const { formatMessage } = useVIntl()
const messages = defineMessages({
selectedCount: {
id: 'content.selection-bar.selected-count',
defaultMessage: '{count} {contentType} selected',
},
selectedCountSimple: {
id: 'content.selection-bar.selected-count-simple',
defaultMessage: '{count, number} selected',
},
enable: {
id: 'content.selection-bar.enable',
defaultMessage: 'Enable',
},
disable: {
id: 'content.selection-bar.disable',
defaultMessage: 'Disable',
},
bulkEnabling: {
id: 'content.selection-bar.bulk.enabling',
defaultMessage: 'Enabling {progress}/{total} {contentType}...',
},
bulkEnablingWaiting: {
id: 'content.selection-bar.bulk.enabling-waiting',
defaultMessage: 'Enabling {contentType}...',
},
bulkDisabling: {
id: 'content.selection-bar.bulk.disabling',
defaultMessage: 'Disabling {progress}/{total} {contentType}...',
},
bulkDisablingWaiting: {
id: 'content.selection-bar.bulk.disabling-waiting',
defaultMessage: 'Disabling {contentType}...',
},
bulkUpdating: {
id: 'content.selection-bar.bulk.updating',
defaultMessage: 'Updating {progress}/{total} {contentType}...',
},
bulkUpdatingWaiting: {
id: 'content.selection-bar.bulk.updating-waiting',
defaultMessage: 'Updating {contentType}...',
},
bulkDeleting: {
id: 'content.selection-bar.bulk.deleting',
defaultMessage: 'Deleting {progress}/{total} {contentType}...',
},
bulkDeletingWaiting: {
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 {
selectedItems: ContentItem[]
contentTypeLabel?: string
isBusy?: boolean
isBulkOperating?: boolean
bulkOperation?: BulkOperationType | null
bulkProgress?: number
bulkTotal?: number
bulkWaiting?: boolean
ariaLabel?: string
}
const props = withDefaults(defineProps<Props>(), {
contentTypeLabel: undefined,
isBusy: false,
isBulkOperating: false,
bulkOperation: null,
bulkProgress: 0,
bulkTotal: 0,
bulkWaiting: false,
ariaLabel: undefined,
})
const emit = defineEmits<{
clear: []
enable: []
disable: []
}>()
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
if (props.contentTypeLabel) {
return formatMessage(messages.selectedCount, {
count,
contentType: `${props.contentTypeLabel}${count === 1 ? '' : 's'}`,
})
}
return formatMessage(messages.selectedCountSimple, { count })
})
const bulkProgressMessage = computed(() => {
if (!props.bulkOperation) return ''
const contentType = props.contentTypeLabel
? `${props.contentTypeLabel}${props.bulkTotal === 1 ? '' : 's'}`
: 'items'
const messageMap = {
enable: props.bulkWaiting ? messages.bulkEnablingWaiting : messages.bulkEnabling,
disable: props.bulkWaiting ? messages.bulkDisablingWaiting : messages.bulkDisabling,
update: props.bulkWaiting ? messages.bulkUpdatingWaiting : messages.bulkUpdating,
delete: props.bulkWaiting ? messages.bulkDeletingWaiting : messages.bulkDeleting,
}
return formatMessage(messageMap[props.bulkOperation], {
progress: props.bulkProgress,
total: props.bulkTotal,
contentType,
})
})
</script>
<template>
<FloatingActionBar :shown="shown" :aria-label="ariaLabel">
<div class="flex items-center gap-0.5">
<span class="px-4 py-2.5 text-base font-semibold text-contrast tabular-nums">
{{ selectedCountText }}
</span>
<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')"
>
<XIcon class="hidden cq-show-icon" />
<span class="bar-label">{{ formatMessage(commonMessages.clearButton) }}</span>
</button>
</ButtonStyled>
</div>
<div v-if="!isBulkOperating" class="ml-auto flex items-center gap-0.5">
<slot name="actions" />
<ButtonStyled type="transparent">
<button
v-tooltip="
allEnabled ? formatMessage(messages.allAlreadyEnabled) : formatMessage(messages.enable)
"
:disabled="isBusy || allEnabled"
@click="emit('enable')"
>
<PowerIcon />
<span class="bar-label">{{ formatMessage(messages.enable) }}</span>
</button>
</ButtonStyled>
<ButtonStyled type="transparent">
<button
v-tooltip="
allDisabled
? formatMessage(messages.allAlreadyDisabled)
: formatMessage(messages.disable)
"
:disabled="isBusy || allDisabled"
@click="emit('disable')"
>
<PowerOffIcon />
<span class="bar-label">{{ formatMessage(messages.disable) }}</span>
</button>
</ButtonStyled>
<slot name="actions-end" />
</div>
<div v-else class="ml-auto flex items-center" aria-live="polite">
<span class="px-4 py-2.5 text-base font-semibold text-secondary tabular-nums">
{{ bulkProgressMessage }}
</span>
</div>
<div v-if="isBulkOperating" class="absolute bottom-0 left-0 right-0 h-1">
<div
class="h-full rounded-l-full bg-brand transition-[width] duration-200 ease-in-out"
:class="{ 'animate-indeterminate': bulkWaiting }"
:style="
!bulkWaiting
? { width: `${bulkTotal > 0 ? (bulkProgress / bulkTotal) * 100 : 0}%` }
: undefined
"
role="progressbar"
:aria-valuenow="bulkWaiting ? undefined : bulkProgress"
:aria-valuemin="0"
:aria-valuemax="bulkTotal"
style="box-shadow: 0px -2px 4px 0px rgba(27, 217, 106, 0.1)"
/>
</div>
</FloatingActionBar>
</template>
<style scoped>
@keyframes indeterminate {
0% {
width: 20%;
margin-left: -20%;
}
100% {
width: 60%;
margin-left: 100%;
}
}
.animate-indeterminate {
animation: indeterminate 1.5s ease-in-out infinite;
}
</style>