Files
ComfyUI_frontend/src/composables/node/useNodeImageUpload.ts
AustinMroz 1c541d8577 Short circuit asset reuploads, simplify node dnd (#11691)
When an output is dragged from the assets panel onto a node, outputs
were being reuploaded. This logic has been simplified to instead
reference the existing asset by resolving the annotated path.

As part of this change, async drop handlers on nodes are also fixed.
Rather than placing obligation of event handling on client code, not
respecting async handlers, or completely ignoring return types, the vue
drop handler will now simply set `app.dragOverNode` and allow the
`document` drop handler to resolve node drag/drop operations without any
of the difficulty from propagation.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11691-Short-circuit-asset-reuploads-simplify-node-dnd-34f6d73d36508157af86e6cf09229781)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-28 18:49:35 -07:00

151 lines
4.0 KiB
TypeScript

import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
import { useNodePaste } from '@/composables/node/useNodePaste'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { ResultItem, ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
const PASTED_IMAGE_EXPIRY_MS = 2000
const UPLOAD_TIMEOUT_MS = 120_000
interface ImageUploadFormFields {
/**
* The folder to upload the file to.
* @example 'input', 'output', 'temp'
*/
type: ResultItemType
}
const uploadFile = async (
file: File,
isPasted: boolean,
formFields: Partial<ImageUploadFormFields> = {}
) => {
const body = new FormData()
body.append('image', file)
if (isPasted) body.append('subfolder', 'pasted')
if (formFields.type) body.append('type', formFields.type)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body,
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
})
if (resp.status !== 200) {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
return
}
const data = await resp.json()
// Update AssetsStore input assets when files are uploaded to input folder
if (formFields.type === 'input' || (!formFields.type && !isPasted)) {
const assetsStore = useAssetsStore()
await assetsStore.updateInputs()
}
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name
}
interface ImageUploadOptions {
fileFilter?: (file: File) => boolean
onUploadComplete: (paths: (string | ResultItem)[]) => void
allow_batch?: boolean
/**
* The file types to accept.
* @example 'image/png,image/jpeg,image/webp,video/webm,video/mp4'
*/
accept?: string
/**
* The folder to upload the file to.
* @example 'input', 'output', 'temp'
*/
folder?: ResultItemType
onUploadStart?: (files: File[]) => void
onUploadError?: () => void
}
/**
* Adds image upload to a node via drag & drop, paste, and file input.
*/
export const useNodeImageUpload = (
node: LGraphNode,
options: ImageUploadOptions
) => {
const { fileFilter, onUploadComplete, allow_batch, accept } = options
const isPastedFile = (file: File): boolean =>
file.name === 'image.png' &&
file.lastModified - Date.now() < PASTED_IMAGE_EXPIRY_MS
const handleUpload = async (file: File) => {
try {
const path = await uploadFile(file, isPastedFile(file), {
type: options.folder
})
if (!path) return
return path
} catch (error) {
if (error instanceof DOMException && error.name === 'TimeoutError') {
useToastStore().addAlert(t('g.uploadTimedOut'))
} else {
useToastStore().addAlert(String(error))
}
}
}
const handleUploadBatch = async (files: File[]) => {
if (node.isUploading) {
useToastStore().addAlert(t('g.uploadAlreadyInProgress'))
return []
}
node.isUploading = true
try {
node.imgs = undefined
node.graph?.setDirtyCanvas(true)
options.onUploadStart?.(files)
const paths = await Promise.all(files.map(handleUpload))
const validPaths = paths.filter((p): p is string => !!p)
if (validPaths.length) {
onUploadComplete(validPaths)
} else {
options.onUploadError?.()
}
return validPaths
} finally {
node.isUploading = false
node.graph?.setDirtyCanvas(true)
}
}
// Handle drag & drop
useNodeDragAndDrop(node, {
fileFilter,
onDrop: handleUploadBatch,
onResultItemDrop: (item) => onUploadComplete([item])
})
// Handle paste
useNodePaste(node, {
fileFilter,
allow_batch,
onPaste: handleUploadBatch
})
// Handle file input
const { openFileSelection } = useNodeFileInput(node, {
fileFilter,
allow_batch,
accept,
onSelect: handleUploadBatch
})
return { openFileSelection, handleUpload }
}