mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: add bulk context menu for multi-asset selection (#7923)
This commit is contained in:
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user