feat: add bulk context menu for multi-asset selection (#7923)

This commit is contained in:
Jin Yi
2026-01-09 15:43:17 +09:00
committed by GitHub
parent 92f21c14d4
commit 43c162a862
5 changed files with 101 additions and 11 deletions

View File

@@ -114,11 +114,15 @@
:output-count="getOutputCount(item)"
:show-delete-button="shouldShowDeleteButton"
:open-context-menu-id="openContextMenuId"
:selected-assets="getSelectedAssets(displayAssets)"
:has-selection="hasSelection"
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
@context-menu-opened="openContextMenuId = item.id"
@bulk-download="handleBulkDownload"
@bulk-delete="handleBulkDelete"
/>
</template>
</VirtualGrid>
@@ -134,7 +138,6 @@
<div ref="selectionCountButtonRef" class="inline-flex w-48">
<Button
variant="secondary"
size="lg"
:class="cn(isCompact && 'text-left')"
@click="handleDeselectAll"
>
@@ -243,11 +246,6 @@ const shouldShowDeleteButton = computed(() => {
return true
})
const getOutputCount = (item: AssetItem): number => {
const count = item.user_metadata?.outputCount
return typeof count === 'number' && count > 0 ? count : 1
}
const shouldShowOutputCount = (item: AssetItem): boolean => {
if (activeTab.value !== 'output' || isInFolderView.value) {
return false
@@ -285,6 +283,8 @@ const {
hasSelection,
clearSelection,
getSelectedAssets,
getOutputCount,
getTotalOutputCount,
activate: activateSelection,
deactivate: deactivateSelection
} = useAssetSelection()
@@ -316,7 +316,7 @@ const isHoveringSelectionCount = useElementHover(selectionCountButtonRef)
// Total output count for all selected assets
const totalOutputCount = computed(() => {
const selectedAssets = getSelectedAssets(displayAssets.value)
return selectedAssets.reduce((sum, asset) => sum + getOutputCount(asset), 0)
return getTotalOutputCount(selectedAssets)
})
const currentAssets = computed(() =>
@@ -537,6 +537,16 @@ const handleDeleteSelected = async () => {
clearSelection()
}
const handleBulkDownload = (assets: AssetItem[]) => {
downloadMultipleAssets(assets)
clearSelection()
}
const handleBulkDelete = async (assets: AssetItem[]) => {
await deleteMultipleAssets(assets)
clearSelection()
}
const handleClearQueue = async () => {
await commandStore.execute('Comfy.ClearPendingTasks')
}

View File

@@ -2410,9 +2410,12 @@
},
"selection": {
"selectedCount": "Assets Selected: {count}",
"multipleSelectedAssets": "Multiple assets selected",
"deselectAll": "Deselect all",
"downloadSelected": "Download",
"downloadSelectedAll": "Download all",
"deleteSelected": "Delete",
"deleteSelectedAll": "Delete all",
"downloadStarted": "Downloading {count} files...",
"downloadsStarted": "Started downloading {count} file(s)",
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",

View File

@@ -127,8 +127,12 @@
:asset-type="assetType"
:file-kind="fileKind"
:show-delete-button="showDeleteButton"
:selected-assets="selectedAssets"
:is-bulk-mode="hasSelection && (selectedAssets?.length ?? 0) > 1"
@zoom="handleZoomClick"
@asset-deleted="emit('asset-deleted')"
@bulk-download="emit('bulk-download', $event)"
@bulk-delete="emit('bulk-delete', $event)"
/>
</template>
@@ -174,7 +178,9 @@ const {
showOutputCount,
outputCount,
showDeleteButton,
openContextMenuId
openContextMenuId,
selectedAssets,
hasSelection
} = defineProps<{
asset?: AssetItem
loading?: boolean
@@ -183,6 +189,8 @@ const {
outputCount?: number
showDeleteButton?: boolean
openContextMenuId?: string | null
selectedAssets?: AssetItem[]
hasSelection?: boolean
}>()
const emit = defineEmits<{
@@ -191,6 +199,8 @@ const emit = defineEmits<{
'output-count-click': []
'asset-deleted': []
'context-menu-opened': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
}>()
const cardContainerRef = ref<HTMLElement>()

View File

@@ -15,11 +15,10 @@
<template #item="{ item, props }">
<Button
variant="secondary"
size="sm"
class="w-full justify-start"
v-bind="props.action"
>
<i :class="item.icon" class="size-4" />
<i v-if="item.icon" :class="item.icon" class="size-4" />
<span>{{
typeof item.label === 'function' ? item.label() : (item.label ?? '')
}}</span>
@@ -45,16 +44,27 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import type { AssetItem } from '../schemas/assetSchema'
import type { AssetContext, MediaKind } from '../schemas/mediaAssetSchema'
const { asset, assetType, fileKind, showDeleteButton } = defineProps<{
const {
asset,
assetType,
fileKind,
showDeleteButton,
selectedAssets,
isBulkMode
} = defineProps<{
asset: AssetItem
assetType: AssetContext['type']
fileKind: MediaKind
showDeleteButton?: boolean
selectedAssets?: AssetItem[]
isBulkMode?: boolean
}>()
const emit = defineEmits<{
zoom: []
'asset-deleted': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
}>()
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
@@ -112,6 +122,45 @@ const contextMenuItems = computed<MenuItem[]>(() => {
const items: MenuItem[] = []
// Check if current asset is part of the selection
const isCurrentAssetSelected = selectedAssets?.some(
(selectedAsset) => selectedAsset.id === asset.id
)
// Bulk mode: Show selected count and bulk actions only if current asset is selected
if (
isBulkMode &&
selectedAssets &&
selectedAssets.length > 0 &&
isCurrentAssetSelected
) {
// Header item showing selected count
items.push({
label: t('mediaAsset.selection.multipleSelectedAssets'),
disabled: true
})
// Bulk Download
items.push({
label: t('mediaAsset.selection.downloadSelectedAll'),
icon: 'icon-[lucide--download]',
command: () => emit('bulk-download', selectedAssets)
})
// Bulk Delete (if allowed)
if (shouldShowDeleteButton.value) {
items.push({
label: t('mediaAsset.selection.deleteSelectedAll'),
icon: 'icon-[lucide--trash-2]',
command: () => emit('bulk-delete', selectedAssets)
})
}
return items
}
// Individual mode: Show all menu options
// Inspect (if not 3D)
if (fileKind !== '3D') {
items.push({

View File

@@ -88,6 +88,22 @@ export function useAssetSelection() {
return allAssets.filter((asset) => selectionStore.isSelected(asset.id))
}
/**
* Get the output count for a single asset
* Same logic as in AssetsSidebarTab.vue
*/
function getOutputCount(item: AssetItem): number {
const count = item.user_metadata?.outputCount
return typeof count === 'number' && count > 0 ? count : 1
}
/**
* Get the total output count for given assets
*/
function getTotalOutputCount(assets: AssetItem[]): number {
return assets.reduce((sum, asset) => sum + getOutputCount(asset), 0)
}
/**
* Activate key event listeners (when sidebar opens)
*/
@@ -116,6 +132,8 @@ export function useAssetSelection() {
selectAll,
clearSelection: () => selectionStore.clearSelection(),
getSelectedAssets,
getOutputCount,
getTotalOutputCount,
reset: () => selectionStore.reset(),
// Lifecycle management