mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
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>
151 lines
4.0 KiB
TypeScript
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 }
|
|
}
|