mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
feat: add bulk actions for workflow operations in media assets (#7992)
## Summary Add bulk action support for Add to Workflow, Open Workflow, and Export Workflow when multiple assets are selected. ## Changes - **What**: Bulk operations for Add to Workflow, Open/Export Workflow in context menu ## Review Focus - Node positioning: Multiple nodes created at same canvas center position (may overlap) - Context menu item ordering without separators <img width="1927" height="921" alt="스크린샷 2026-01-13 오후 12 54 52" src="https://github.com/user-attachments/assets/6f079232-1b24-4f02-810f-6e396916bb71" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7992-feat-add-bulk-actions-for-workflow-operations-in-media-assets-2e76d73d365081aa90c6fdb5039c9a3e) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -204,6 +204,9 @@
|
|||||||
@asset-deleted="refreshAssets"
|
@asset-deleted="refreshAssets"
|
||||||
@bulk-download="handleBulkDownload"
|
@bulk-download="handleBulkDownload"
|
||||||
@bulk-delete="handleBulkDelete"
|
@bulk-delete="handleBulkDelete"
|
||||||
|
@bulk-add-to-workflow="handleBulkAddToWorkflow"
|
||||||
|
@bulk-open-workflow="handleBulkOpenWorkflow"
|
||||||
|
@bulk-export-workflow="handleBulkExportWorkflow"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -321,7 +324,13 @@ const {
|
|||||||
deactivate: deactivateSelection
|
deactivate: deactivateSelection
|
||||||
} = useAssetSelection()
|
} = useAssetSelection()
|
||||||
|
|
||||||
const { downloadMultipleAssets, deleteMultipleAssets } = useMediaAssetActions()
|
const {
|
||||||
|
downloadMultipleAssets,
|
||||||
|
deleteMultipleAssets,
|
||||||
|
addMultipleToWorkflow,
|
||||||
|
openMultipleWorkflows,
|
||||||
|
exportMultipleWorkflows
|
||||||
|
} = useMediaAssetActions()
|
||||||
|
|
||||||
// Footer responsive behavior
|
// Footer responsive behavior
|
||||||
const footerRef = ref<HTMLElement | null>(null)
|
const footerRef = ref<HTMLElement | null>(null)
|
||||||
@@ -607,6 +616,21 @@ const handleBulkDelete = async (assets: AssetItem[]) => {
|
|||||||
clearSelection()
|
clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBulkAddToWorkflow = async (assets: AssetItem[]) => {
|
||||||
|
await addMultipleToWorkflow(assets)
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBulkOpenWorkflow = async (assets: AssetItem[]) => {
|
||||||
|
await openMultipleWorkflows(assets)
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBulkExportWorkflow = async (assets: AssetItem[]) => {
|
||||||
|
await exportMultipleWorkflows(assets)
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
const handleClearQueue = async () => {
|
const handleClearQueue = async () => {
|
||||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2414,7 +2414,7 @@
|
|||||||
"zoom": "Zoom in",
|
"zoom": "Zoom in",
|
||||||
"moreOptions": "More options",
|
"moreOptions": "More options",
|
||||||
"seeMoreOutputs": "See more outputs",
|
"seeMoreOutputs": "See more outputs",
|
||||||
"addToWorkflow": "Add to current workflow",
|
"insertAsNodeInWorkflow": "Insert as node in workflow",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"openWorkflow": "Open as workflow in new tab",
|
"openWorkflow": "Open as workflow in new tab",
|
||||||
"exportWorkflow": "Export workflow",
|
"exportWorkflow": "Export workflow",
|
||||||
@@ -2435,11 +2435,23 @@
|
|||||||
"downloadSelectedAll": "Download all",
|
"downloadSelectedAll": "Download all",
|
||||||
"deleteSelected": "Delete",
|
"deleteSelected": "Delete",
|
||||||
"deleteSelectedAll": "Delete all",
|
"deleteSelectedAll": "Delete all",
|
||||||
|
"insertAllAssetsAsNodes": "Insert all assets as nodes",
|
||||||
|
"openWorkflowAll": "Open all workflows",
|
||||||
|
"exportWorkflowAll": "Export all workflows",
|
||||||
"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",
|
||||||
"failedToDeleteAssets": "Failed to delete selected assets",
|
"failedToDeleteAssets": "Failed to delete selected assets",
|
||||||
"partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed"
|
"partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed",
|
||||||
|
"nodesAddedToWorkflow": "{count} node(s) added to workflow",
|
||||||
|
"failedToAddNodes": "Failed to add nodes to workflow",
|
||||||
|
"partialAddNodesSuccess": "{succeeded} added successfully, {failed} failed",
|
||||||
|
"workflowsOpened": "{count} workflow(s) opened in new tabs",
|
||||||
|
"noWorkflowsFound": "No workflow data found in selected assets",
|
||||||
|
"partialWorkflowsOpened": "{succeeded} workflow(s) opened, {failed} failed",
|
||||||
|
"workflowsExported": "{count} workflow(s) exported successfully",
|
||||||
|
"noWorkflowsToExport": "No workflow data found to export",
|
||||||
|
"partialWorkflowsExported": "{succeeded} exported successfully, {failed} failed"
|
||||||
},
|
},
|
||||||
"noJobIdFound": "No job ID found for this asset",
|
"noJobIdFound": "No job ID found for this asset",
|
||||||
"unsupportedFileType": "Unsupported file type for loader node",
|
"unsupportedFileType": "Unsupported file type for loader node",
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ const emit = defineEmits<{
|
|||||||
'bulk-download': [assets: AssetItem[]]
|
'bulk-download': [assets: AssetItem[]]
|
||||||
'bulk-delete': [assets: AssetItem[]]
|
'bulk-delete': [assets: AssetItem[]]
|
||||||
hide: []
|
hide: []
|
||||||
|
'bulk-add-to-workflow': [assets: AssetItem[]]
|
||||||
|
'bulk-open-workflow': [assets: AssetItem[]]
|
||||||
|
'bulk-export-workflow': [assets: AssetItem[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
|
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
|
||||||
@@ -142,6 +145,27 @@ const contextMenuItems = computed<MenuItem[]>(() => {
|
|||||||
disabled: true
|
disabled: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Bulk Add to Workflow
|
||||||
|
items.push({
|
||||||
|
label: t('mediaAsset.selection.insertAllAssetsAsNodes'),
|
||||||
|
icon: 'icon-[comfy--node]',
|
||||||
|
command: () => emit('bulk-add-to-workflow', selectedAssets)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bulk Open Workflow
|
||||||
|
items.push({
|
||||||
|
label: t('mediaAsset.selection.openWorkflowAll'),
|
||||||
|
icon: 'icon-[comfy--workflow]',
|
||||||
|
command: () => emit('bulk-open-workflow', selectedAssets)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bulk Export Workflow
|
||||||
|
items.push({
|
||||||
|
label: t('mediaAsset.selection.exportWorkflowAll'),
|
||||||
|
icon: 'icon-[lucide--file-output]',
|
||||||
|
command: () => emit('bulk-export-workflow', selectedAssets)
|
||||||
|
})
|
||||||
|
|
||||||
// Bulk Download
|
// Bulk Download
|
||||||
items.push({
|
items.push({
|
||||||
label: t('mediaAsset.selection.downloadSelectedAll'),
|
label: t('mediaAsset.selection.downloadSelectedAll'),
|
||||||
@@ -175,7 +199,7 @@ const contextMenuItems = computed<MenuItem[]>(() => {
|
|||||||
// Add to workflow (conditional)
|
// Add to workflow (conditional)
|
||||||
if (showAddToWorkflow.value) {
|
if (showAddToWorkflow.value) {
|
||||||
items.push({
|
items.push({
|
||||||
label: t('mediaAsset.actions.addToWorkflow'),
|
label: t('mediaAsset.actions.insertAsNodeInWorkflow'),
|
||||||
icon: 'icon-[comfy--node]',
|
icon: 'icon-[comfy--node]',
|
||||||
command: () => actions.addWorkflow(asset)
|
command: () => actions.addWorkflow(asset)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -365,6 +365,208 @@ export function useMediaAssetActions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add multiple assets to the current workflow
|
||||||
|
* Creates loader nodes for each asset
|
||||||
|
*/
|
||||||
|
const addMultipleToWorkflow = async (assets: AssetItem[]) => {
|
||||||
|
if (!assets || assets.length === 0) return
|
||||||
|
|
||||||
|
const NODE_OFFSET = 50
|
||||||
|
let nodeIndex = 0
|
||||||
|
let succeeded = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
const { nodeType, widgetName } = detectNodeTypeFromFilename(asset.name)
|
||||||
|
|
||||||
|
if (!nodeType || !widgetName) {
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeDef = nodeDefStore.nodeDefsByName[nodeType]
|
||||||
|
if (!nodeDef) {
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = litegraphService.getCanvasCenter()
|
||||||
|
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
||||||
|
pos: [
|
||||||
|
center[0] + nodeIndex * NODE_OFFSET,
|
||||||
|
center[1] + nodeIndex * NODE_OFFSET
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||||
|
const assetType = getAssetType(asset, 'input')
|
||||||
|
|
||||||
|
const annotated = createAnnotatedPath(
|
||||||
|
{
|
||||||
|
filename: asset.name,
|
||||||
|
subfolder: metadata?.subfolder || '',
|
||||||
|
type: isResultItemType(assetType) ? assetType : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootFolder: isResultItemType(assetType) ? assetType : undefined
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||||
|
if (widget) {
|
||||||
|
widget.value = annotated
|
||||||
|
widget.callback?.(annotated)
|
||||||
|
}
|
||||||
|
node.graph?.setDirtyCanvas(true, true)
|
||||||
|
succeeded++
|
||||||
|
nodeIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('g.success'),
|
||||||
|
detail: t('mediaAsset.selection.nodesAddedToWorkflow', {
|
||||||
|
count: succeeded
|
||||||
|
}),
|
||||||
|
life: 2000
|
||||||
|
})
|
||||||
|
} else if (succeeded === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('g.error'),
|
||||||
|
detail: t('mediaAsset.selection.failedToAddNodes'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('g.warning'),
|
||||||
|
detail: t('mediaAsset.selection.partialAddNodesSuccess', {
|
||||||
|
succeeded,
|
||||||
|
failed
|
||||||
|
}),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open workflows from multiple assets in new tabs
|
||||||
|
*/
|
||||||
|
const openMultipleWorkflows = async (assets: AssetItem[]) => {
|
||||||
|
if (!assets || assets.length === 0) return
|
||||||
|
|
||||||
|
let succeeded = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
try {
|
||||||
|
const { workflow, filename } = await extractWorkflowFromAsset(asset)
|
||||||
|
const result = await workflowActions.openWorkflowAction(
|
||||||
|
workflow,
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
succeeded++
|
||||||
|
} else {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('g.success'),
|
||||||
|
detail: t('mediaAsset.selection.workflowsOpened', { count: succeeded }),
|
||||||
|
life: 2000
|
||||||
|
})
|
||||||
|
} else if (succeeded === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('g.warning'),
|
||||||
|
detail: t('mediaAsset.selection.noWorkflowsFound'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('g.warning'),
|
||||||
|
detail: t('mediaAsset.selection.partialWorkflowsOpened', {
|
||||||
|
succeeded,
|
||||||
|
failed
|
||||||
|
}),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export workflows from multiple assets as JSON files
|
||||||
|
*/
|
||||||
|
const exportMultipleWorkflows = async (assets: AssetItem[]) => {
|
||||||
|
if (!assets || assets.length === 0) return
|
||||||
|
|
||||||
|
let succeeded = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
try {
|
||||||
|
const { workflow, filename } = await extractWorkflowFromAsset(asset)
|
||||||
|
const result = await workflowActions.exportWorkflowAction(
|
||||||
|
workflow,
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
succeeded++
|
||||||
|
} else {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('g.success'),
|
||||||
|
detail: t('mediaAsset.selection.workflowsExported', {
|
||||||
|
count: succeeded
|
||||||
|
}),
|
||||||
|
life: 2000
|
||||||
|
})
|
||||||
|
} else if (succeeded === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('g.warning'),
|
||||||
|
detail: t('mediaAsset.selection.noWorkflowsToExport'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('g.warning'),
|
||||||
|
detail: t('mediaAsset.selection.partialWorkflowsExported', {
|
||||||
|
succeeded,
|
||||||
|
failed
|
||||||
|
}),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete multiple assets with confirmation dialog
|
* Delete multiple assets with confirmation dialog
|
||||||
* @param assets Array of assets to delete
|
* @param assets Array of assets to delete
|
||||||
@@ -482,7 +684,10 @@ export function useMediaAssetActions() {
|
|||||||
deleteMultipleAssets,
|
deleteMultipleAssets,
|
||||||
copyJobId,
|
copyJobId,
|
||||||
addWorkflow,
|
addWorkflow,
|
||||||
|
addMultipleToWorkflow,
|
||||||
openWorkflow,
|
openWorkflow,
|
||||||
exportWorkflow
|
openMultipleWorkflows,
|
||||||
|
exportWorkflow,
|
||||||
|
exportMultipleWorkflows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user