mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 16:40:05 +00:00
feat: add bulk context menu for multi-asset selection (#7923)
This commit is contained in:
@@ -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