mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +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)"
|
:output-count="getOutputCount(item)"
|
||||||
:show-delete-button="shouldShowDeleteButton"
|
:show-delete-button="shouldShowDeleteButton"
|
||||||
:open-context-menu-id="openContextMenuId"
|
:open-context-menu-id="openContextMenuId"
|
||||||
|
:selected-assets="getSelectedAssets(displayAssets)"
|
||||||
|
:has-selection="hasSelection"
|
||||||
@click="handleAssetSelect(item)"
|
@click="handleAssetSelect(item)"
|
||||||
@zoom="handleZoomClick(item)"
|
@zoom="handleZoomClick(item)"
|
||||||
@output-count-click="enterFolderView(item)"
|
@output-count-click="enterFolderView(item)"
|
||||||
@asset-deleted="refreshAssets"
|
@asset-deleted="refreshAssets"
|
||||||
@context-menu-opened="openContextMenuId = item.id"
|
@context-menu-opened="openContextMenuId = item.id"
|
||||||
|
@bulk-download="handleBulkDownload"
|
||||||
|
@bulk-delete="handleBulkDelete"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VirtualGrid>
|
</VirtualGrid>
|
||||||
@@ -134,7 +138,6 @@
|
|||||||
<div ref="selectionCountButtonRef" class="inline-flex w-48">
|
<div ref="selectionCountButtonRef" class="inline-flex w-48">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
|
||||||
:class="cn(isCompact && 'text-left')"
|
:class="cn(isCompact && 'text-left')"
|
||||||
@click="handleDeselectAll"
|
@click="handleDeselectAll"
|
||||||
>
|
>
|
||||||
@@ -243,11 +246,6 @@ const shouldShowDeleteButton = computed(() => {
|
|||||||
return true
|
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 => {
|
const shouldShowOutputCount = (item: AssetItem): boolean => {
|
||||||
if (activeTab.value !== 'output' || isInFolderView.value) {
|
if (activeTab.value !== 'output' || isInFolderView.value) {
|
||||||
return false
|
return false
|
||||||
@@ -285,6 +283,8 @@ const {
|
|||||||
hasSelection,
|
hasSelection,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
getSelectedAssets,
|
getSelectedAssets,
|
||||||
|
getOutputCount,
|
||||||
|
getTotalOutputCount,
|
||||||
activate: activateSelection,
|
activate: activateSelection,
|
||||||
deactivate: deactivateSelection
|
deactivate: deactivateSelection
|
||||||
} = useAssetSelection()
|
} = useAssetSelection()
|
||||||
@@ -316,7 +316,7 @@ const isHoveringSelectionCount = useElementHover(selectionCountButtonRef)
|
|||||||
// Total output count for all selected assets
|
// Total output count for all selected assets
|
||||||
const totalOutputCount = computed(() => {
|
const totalOutputCount = computed(() => {
|
||||||
const selectedAssets = getSelectedAssets(displayAssets.value)
|
const selectedAssets = getSelectedAssets(displayAssets.value)
|
||||||
return selectedAssets.reduce((sum, asset) => sum + getOutputCount(asset), 0)
|
return getTotalOutputCount(selectedAssets)
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentAssets = computed(() =>
|
const currentAssets = computed(() =>
|
||||||
@@ -537,6 +537,16 @@ const handleDeleteSelected = async () => {
|
|||||||
clearSelection()
|
clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBulkDownload = (assets: AssetItem[]) => {
|
||||||
|
downloadMultipleAssets(assets)
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBulkDelete = async (assets: AssetItem[]) => {
|
||||||
|
await deleteMultipleAssets(assets)
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
const handleClearQueue = async () => {
|
const handleClearQueue = async () => {
|
||||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2410,9 +2410,12 @@
|
|||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"selectedCount": "Assets Selected: {count}",
|
"selectedCount": "Assets Selected: {count}",
|
||||||
|
"multipleSelectedAssets": "Multiple assets selected",
|
||||||
"deselectAll": "Deselect all",
|
"deselectAll": "Deselect all",
|
||||||
"downloadSelected": "Download",
|
"downloadSelected": "Download",
|
||||||
|
"downloadSelectedAll": "Download all",
|
||||||
"deleteSelected": "Delete",
|
"deleteSelected": "Delete",
|
||||||
|
"deleteSelectedAll": "Delete all",
|
||||||
"downloadStarted": "Downloading {count} files...",
|
"downloadStarted": "Downloading {count} files...",
|
||||||
"downloadsStarted": "Started downloading {count} file(s)",
|
"downloadsStarted": "Started downloading {count} file(s)",
|
||||||
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
|
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
|
||||||
|
|||||||
@@ -127,8 +127,12 @@
|
|||||||
:asset-type="assetType"
|
:asset-type="assetType"
|
||||||
:file-kind="fileKind"
|
:file-kind="fileKind"
|
||||||
:show-delete-button="showDeleteButton"
|
:show-delete-button="showDeleteButton"
|
||||||
|
:selected-assets="selectedAssets"
|
||||||
|
:is-bulk-mode="hasSelection && (selectedAssets?.length ?? 0) > 1"
|
||||||
@zoom="handleZoomClick"
|
@zoom="handleZoomClick"
|
||||||
@asset-deleted="emit('asset-deleted')"
|
@asset-deleted="emit('asset-deleted')"
|
||||||
|
@bulk-download="emit('bulk-download', $event)"
|
||||||
|
@bulk-delete="emit('bulk-delete', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -174,7 +178,9 @@ const {
|
|||||||
showOutputCount,
|
showOutputCount,
|
||||||
outputCount,
|
outputCount,
|
||||||
showDeleteButton,
|
showDeleteButton,
|
||||||
openContextMenuId
|
openContextMenuId,
|
||||||
|
selectedAssets,
|
||||||
|
hasSelection
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
asset?: AssetItem
|
asset?: AssetItem
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
@@ -183,6 +189,8 @@ const {
|
|||||||
outputCount?: number
|
outputCount?: number
|
||||||
showDeleteButton?: boolean
|
showDeleteButton?: boolean
|
||||||
openContextMenuId?: string | null
|
openContextMenuId?: string | null
|
||||||
|
selectedAssets?: AssetItem[]
|
||||||
|
hasSelection?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -191,6 +199,8 @@ const emit = defineEmits<{
|
|||||||
'output-count-click': []
|
'output-count-click': []
|
||||||
'asset-deleted': []
|
'asset-deleted': []
|
||||||
'context-menu-opened': []
|
'context-menu-opened': []
|
||||||
|
'bulk-download': [assets: AssetItem[]]
|
||||||
|
'bulk-delete': [assets: AssetItem[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const cardContainerRef = ref<HTMLElement>()
|
const cardContainerRef = ref<HTMLElement>()
|
||||||
|
|||||||
@@ -15,11 +15,10 @@
|
|||||||
<template #item="{ item, props }">
|
<template #item="{ item, props }">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
|
||||||
class="w-full justify-start"
|
class="w-full justify-start"
|
||||||
v-bind="props.action"
|
v-bind="props.action"
|
||||||
>
|
>
|
||||||
<i :class="item.icon" class="size-4" />
|
<i v-if="item.icon" :class="item.icon" class="size-4" />
|
||||||
<span>{{
|
<span>{{
|
||||||
typeof item.label === 'function' ? item.label() : (item.label ?? '')
|
typeof item.label === 'function' ? item.label() : (item.label ?? '')
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -45,16 +44,27 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
|||||||
import type { AssetItem } from '../schemas/assetSchema'
|
import type { AssetItem } from '../schemas/assetSchema'
|
||||||
import type { AssetContext, MediaKind } from '../schemas/mediaAssetSchema'
|
import type { AssetContext, MediaKind } from '../schemas/mediaAssetSchema'
|
||||||
|
|
||||||
const { asset, assetType, fileKind, showDeleteButton } = defineProps<{
|
const {
|
||||||
|
asset,
|
||||||
|
assetType,
|
||||||
|
fileKind,
|
||||||
|
showDeleteButton,
|
||||||
|
selectedAssets,
|
||||||
|
isBulkMode
|
||||||
|
} = defineProps<{
|
||||||
asset: AssetItem
|
asset: AssetItem
|
||||||
assetType: AssetContext['type']
|
assetType: AssetContext['type']
|
||||||
fileKind: MediaKind
|
fileKind: MediaKind
|
||||||
showDeleteButton?: boolean
|
showDeleteButton?: boolean
|
||||||
|
selectedAssets?: AssetItem[]
|
||||||
|
isBulkMode?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
zoom: []
|
zoom: []
|
||||||
'asset-deleted': []
|
'asset-deleted': []
|
||||||
|
'bulk-download': [assets: AssetItem[]]
|
||||||
|
'bulk-delete': [assets: AssetItem[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
|
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
|
||||||
@@ -112,6 +122,45 @@ const contextMenuItems = computed<MenuItem[]>(() => {
|
|||||||
|
|
||||||
const items: 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)
|
// Inspect (if not 3D)
|
||||||
if (fileKind !== '3D') {
|
if (fileKind !== '3D') {
|
||||||
items.push({
|
items.push({
|
||||||
|
|||||||
@@ -88,6 +88,22 @@ export function useAssetSelection() {
|
|||||||
return allAssets.filter((asset) => selectionStore.isSelected(asset.id))
|
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)
|
* Activate key event listeners (when sidebar opens)
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +132,8 @@ export function useAssetSelection() {
|
|||||||
selectAll,
|
selectAll,
|
||||||
clearSelection: () => selectionStore.clearSelection(),
|
clearSelection: () => selectionStore.clearSelection(),
|
||||||
getSelectedAssets,
|
getSelectedAssets,
|
||||||
|
getOutputCount,
|
||||||
|
getTotalOutputCount,
|
||||||
reset: () => selectionStore.reset(),
|
reset: () => selectionStore.reset(),
|
||||||
|
|
||||||
// Lifecycle management
|
// Lifecycle management
|
||||||
|
|||||||
Reference in New Issue
Block a user