Add support for dragging in multiple workflow files at once (#8757)

## Summary

Allows users to drag in multiple files that are/have embedded workflows
and loads each of them as tabs.
Previously it would only load the first one.

## Changes

- **What**: 
- process all files from drop event
- add defered errors so you don't get errors for non-visible workflows

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8757-Add-support-for-dragging-in-multiple-workflow-files-at-once-3026d73d365081c096e9dfb18ba01253)
by [Unito](https://www.unito.io)
This commit is contained in:
pythongosssss
2026-02-17 07:45:22 +00:00
committed by GitHub
parent efe78b799f
commit f5f5a77435
9 changed files with 429 additions and 107 deletions

View File

@@ -85,11 +85,7 @@ describe('ComfyApp', () => {
const file1 = createTestFile('test1.png', 'image/png')
const file2 = createTestFile('test2.jpg', 'image/jpeg')
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file1)
dataTransfer.items.add(file2)
const { files } = dataTransfer
const files = [file1, file2]
await app.handleFileList(files)
@@ -110,26 +106,21 @@ describe('ComfyApp', () => {
vi.mocked(createNode).mockResolvedValue(null)
const file = createTestFile('test.png', 'image/png')
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
await app.handleFileList(dataTransfer.files)
await app.handleFileList([file])
expect(mockCanvas.selectItems).not.toHaveBeenCalled()
expect(mockNode1.connect).not.toHaveBeenCalled()
})
it('should handle empty file list', async () => {
const dataTransfer = new DataTransfer()
await expect(app.handleFileList(dataTransfer.files)).rejects.toThrow()
await expect(app.handleFileList([])).rejects.toThrow()
})
it('should not process unsupported file types', async () => {
const invalidFile = createTestFile('test.pdf', 'application/pdf')
const dataTransfer = new DataTransfer()
dataTransfer.items.add(invalidFile)
await app.handleFileList(dataTransfer.files)
await app.handleFileList([invalidFile])
expect(pasteImageNodes).not.toHaveBeenCalled()
expect(createNode).not.toHaveBeenCalled()

View File

@@ -24,6 +24,7 @@ import { useTelemetry } from '@/platform/telemetry'
import type { WorkflowOpenSource } from '@/platform/telemetry/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import type { PendingWarnings } from '@/platform/workflow/management/stores/comfyWorkflow'
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowValidation } from '@/platform/workflow/validation/composables/useWorkflowValidation'
import {
@@ -107,7 +108,7 @@ import { ComfyAppMenu } from './ui/menu/index'
import { clone } from './utils'
import { type ComfyWidgetConstructor } from './widgets'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { extractFileFromDragEvent } from '@/utils/eventUtils'
import { extractFilesFromDragEvent, hasImageType } from '@/utils/eventUtils'
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste'
@@ -550,22 +551,25 @@ export class ComfyApp {
// If you drag multiple files it will call it multiple times with the same file
if (await n?.onDragDrop?.(event)) return
const fileMaybe = await extractFileFromDragEvent(event)
if (!fileMaybe) return
const files = await extractFilesFromDragEvent(event)
if (files.length === 0) return
const workspace = useWorkspaceStore()
try {
workspace.spinner = true
if (fileMaybe instanceof File) {
await this.handleFile(fileMaybe, 'file_drop')
}
if (fileMaybe instanceof FileList) {
await this.handleFileList(fileMaybe)
if (files.length > 1 && files.every(hasImageType)) {
await this.handleFileList(files)
} else {
for (const file of files) {
await this.handleFile(file, 'file_drop', {
deferWarnings: true
})
}
}
} finally {
workspace.spinner = false
}
useWorkflowService().showPendingWarnings()
} catch (error: unknown) {
useToastStore().addAlert(t('toastMessages.dropFileError', { error }))
}
@@ -1063,18 +1067,6 @@ export class ComfyApp {
}
}
private showMissingModelsError(
missingModels: ModelFile[],
paths: Record<string, string[]>
): void {
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
useDialogService().showMissingModelsWarning({
missingModels,
paths
})
}
}
async loadGraphData(
graphData?: ComfyWorkflowJSON,
clean: boolean = true,
@@ -1085,13 +1077,15 @@ export class ComfyApp {
showMissingModelsDialog?: boolean
checkForRerouteMigration?: boolean
openSource?: WorkflowOpenSource
deferWarnings?: boolean
} = {}
) {
const {
showMissingNodesDialog = true,
showMissingModelsDialog = true,
checkForRerouteMigration = false,
openSource
openSource,
deferWarnings = false
} = options
useWorkflowService().beforeLoadNewGraph()
@@ -1334,13 +1328,6 @@ export class ComfyApp {
useExtensionService().invokeExtensions('loadedGraphNode', node)
})
if (missingNodeTypes.length && showMissingNodesDialog) {
this.showMissingNodesError(missingNodeTypes)
}
if (missingModels.length && showMissingModelsDialog) {
const paths = await api.getFolderPaths()
this.showMissingModelsError(missingModels, paths)
}
await useExtensionService().invokeExtensionsAsync(
'afterConfigureGraph',
missingNodeTypes
@@ -1359,6 +1346,27 @@ export class ComfyApp {
workflow,
this.rootGraph.serialize() as unknown as ComfyWorkflowJSON
)
// Store pending warnings on the workflow for deferred display
const activeWf = useWorkspaceStore().workflow.activeWorkflow
if (activeWf) {
const warnings: PendingWarnings = {}
if (missingNodeTypes.length && showMissingNodesDialog) {
warnings.missingNodeTypes = missingNodeTypes
}
if (missingModels.length && showMissingModelsDialog) {
const paths = await api.getFolderPaths()
warnings.missingModels = { missingModels: missingModels, paths }
}
if (warnings.missingNodeTypes || warnings.missingModels) {
activeWf.pendingWarnings = warnings
}
}
if (!deferWarnings) {
useWorkflowService().showPendingWarnings()
}
requestAnimationFrame(() => {
this.canvas.setDirty(true, true)
})
@@ -1500,7 +1508,11 @@ export class ComfyApp {
* Loads workflow data from the specified file
* @param {File} file
*/
async handleFile(file: File, openSource?: WorkflowOpenSource) {
async handleFile(
file: File,
openSource?: WorkflowOpenSource,
options?: { deferWarnings?: boolean }
) {
const fileName = file.name.replace(/\.\w+$/, '') // Strip file extension
const workflowData = await getWorkflowDataFromFile(file)
const { workflow, prompt, parameters, templates } = workflowData ?? {}
@@ -1543,7 +1555,8 @@ export class ComfyApp {
!Array.isArray(workflowObj)
) {
await this.loadGraphData(workflowObj, true, true, fileName, {
openSource
openSource,
deferWarnings: options?.deferWarnings
})
return
} else {
@@ -1591,7 +1604,7 @@ export class ComfyApp {
* Loads multiple files, connects to a batch node, and selects them
* @param {FileList} fileList
*/
async handleFileList(fileList: FileList) {
async handleFileList(fileList: File[]) {
if (fileList[0].type.startsWith('image')) {
const imageNodes = await pasteImageNodes(this.canvas, fileList)
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')