mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 01:09:46 +00:00
feat: Add multi-select support for media assets (#6256)
## Summary
Implements file explorer-style multi-selection functionality for media
assets in the AssetsSidebarTab component.
## Changes
### Multi-Selection Interactions
- **Normal click**: Single selection (clears previous, selects new)
- **Shift + click**: Range selection (from last selected to current)
- **Ctrl/Cmd + click**: Toggle individual selection
### State Management
- Added `assetSelectionStore` to manage selected asset IDs using Set
- Created `useAssetSelection` composable for selection logic and
keyboard state
### UI Enhancements
- Display selection count in footer (output tab only)
- Interactive selection count that shows "Deselect all" on hover
- Added bulk action buttons for download/delete (UI only)
### Translation Keys
Added new keys under `mediaAsset.selection`:
- `selectedCount`: "{count} selected"
- `deselectAll`: "Deselect all"
- `downloadSelected`: "Download"
- `deleteSelected`: "Delete"
## Test Plan
- [x] Open Assets sidebar tab
- [x] Switch to Generated tab
- [x] Test single selection with normal click
- [x] Test range selection with Shift + click
- [x] Test toggle selection with Ctrl/Cmd + click
- [x] Verify selection count updates correctly
- [x] Test hover interaction on selection count
- [x] Click "Deselect all" to clear selection
- [x] Test bulk action buttons (UI only)
## Notes
- Bulk download/delete functionality to be implemented in separate PR
- Selection UI currently only shows in output (Generated) tab
[screen-capture.webm](https://github.com/user-attachments/assets/740315bd-9254-4af3-a0be-10846d810d65)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<IconGroup>
|
||||
<IconButton v-if="showDeleteButton" size="sm" @click="handleDelete">
|
||||
<IconButton v-if="shouldShowDeleteButton" size="sm" @click="handleDelete">
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</IconButton>
|
||||
<IconButton size="sm" @click="handleDownload">
|
||||
@@ -14,6 +14,7 @@
|
||||
<template #default="{ close }">
|
||||
<MediaAssetMoreMenu
|
||||
:close="close"
|
||||
:show-delete-button="showDeleteButton"
|
||||
@inspect="emit('inspect')"
|
||||
@asset-deleted="emit('asset-deleted')"
|
||||
/>
|
||||
@@ -34,6 +35,10 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetMoreMenu from './MediaAssetMoreMenu.vue'
|
||||
|
||||
const { showDeleteButton } = defineProps<{
|
||||
showDeleteButton?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
menuStateChanged: [isOpen: boolean]
|
||||
inspect: []
|
||||
@@ -47,10 +52,12 @@ const assetType = computed(() => {
|
||||
return context?.value?.type || asset.value?.tags?.[0] || 'output'
|
||||
})
|
||||
|
||||
const showDeleteButton = computed(() => {
|
||||
return (
|
||||
const shouldShowDeleteButton = computed(() => {
|
||||
const propAllows = showDeleteButton ?? true
|
||||
const typeAllows =
|
||||
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
|
||||
)
|
||||
|
||||
return propAllows && typeAllows
|
||||
})
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
variant="ghost"
|
||||
rounded="lg"
|
||||
:class="containerClasses"
|
||||
@click="handleCardClick"
|
||||
@keydown.enter="handleCardClick"
|
||||
@keydown.space.prevent="handleCardClick"
|
||||
>
|
||||
<template #top>
|
||||
<CardTop
|
||||
@@ -50,6 +47,7 @@
|
||||
<!-- Actions overlay (top-left) - show on hover or when menu is open -->
|
||||
<template v-if="showActionsOverlay" #top-left>
|
||||
<MediaAssetActions
|
||||
:show-delete-button="showDeleteButton ?? true"
|
||||
@menu-state-changed="isMenuOpen = $event"
|
||||
@inspect="handleZoomClick"
|
||||
@asset-deleted="handleAssetDelete"
|
||||
@@ -174,12 +172,20 @@ function getBottomComponent(kind: MediaKind) {
|
||||
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
|
||||
}
|
||||
|
||||
const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
|
||||
const {
|
||||
asset,
|
||||
loading,
|
||||
selected,
|
||||
showOutputCount,
|
||||
outputCount,
|
||||
showDeleteButton
|
||||
} = defineProps<{
|
||||
asset?: AssetItem
|
||||
loading?: boolean
|
||||
selected?: boolean
|
||||
showOutputCount?: boolean
|
||||
outputCount?: number
|
||||
showDeleteButton?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -312,12 +318,6 @@ const showFileFormatChip = computed(
|
||||
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
|
||||
)
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (adaptedAsset.value) {
|
||||
actions.selectAsset(adaptedAsset.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOverlayMouseEnter = () => {
|
||||
isOverlayHovered.value = true
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<MediaAssetButtonDivider v-if="showCopyJobId && showDeleteButton" />
|
||||
<MediaAssetButtonDivider v-if="showCopyJobId && shouldShowDeleteButton" />
|
||||
|
||||
<IconTextButton
|
||||
v-if="showDeleteButton"
|
||||
v-if="shouldShowDeleteButton"
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Delete"
|
||||
@@ -101,8 +101,9 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetButtonDivider from './MediaAssetButtonDivider.vue'
|
||||
|
||||
const { close } = defineProps<{
|
||||
const { close, showDeleteButton } = defineProps<{
|
||||
close: () => void
|
||||
showDeleteButton?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -124,13 +125,12 @@ const showCopyJobId = computed(() => {
|
||||
return assetType.value !== 'input'
|
||||
})
|
||||
|
||||
// Delete button should be shown for:
|
||||
// - All output files (can be deleted via history)
|
||||
// - Input files only in cloud environment
|
||||
const showDeleteButton = computed(() => {
|
||||
return (
|
||||
const shouldShowDeleteButton = computed(() => {
|
||||
const propAllows = showDeleteButton ?? true
|
||||
const typeAllows =
|
||||
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
|
||||
)
|
||||
|
||||
return propAllows && typeAllows
|
||||
})
|
||||
|
||||
const handleInspect = () => {
|
||||
|
||||
Reference in New Issue
Block a user