From dfb6b6b35d4e0a861d575cce42bc141be7c9434c Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 23 Jan 2026 14:03:10 -0600 Subject: [PATCH 01/15] Batch Drag & Drop Images --- src/composables/usePaste.test.ts | 153 +++++++++++--- src/composables/usePaste.ts | 76 +++++-- src/lib/litegraph/src/LGraph.test.ts | 11 + src/lib/litegraph/src/LGraph.ts | 3 +- src/scripts/app.test.ts | 271 +++++++++++++++++++++++++ src/scripts/app.ts | 57 +++++- src/utils/__tests__/eventUtils.test.ts | 39 ++++ src/utils/eventUtils.ts | 16 +- src/utils/litegraphUtil.test.ts | 114 ++++++++++- src/utils/litegraphUtil.ts | 37 +++- 10 files changed, 723 insertions(+), 54 deletions(-) create mode 100644 src/scripts/app.test.ts diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index 4e1ac3503c5..f380ea27c96 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -7,8 +7,13 @@ import type { } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { app } from '@/scripts/app' -import { isImageNode } from '@/utils/litegraphUtil' -import { pasteImageNode, usePaste } from './usePaste' +import { createNode, isImageNode } from '@/utils/litegraphUtil' +import { + cloneDataTransfer, + pasteImageNode, + pasteImageNodes, + usePaste +} from './usePaste' function createMockNode() { return { @@ -86,6 +91,7 @@ vi.mock('@/lib/litegraph/src/litegraph', () => ({ })) vi.mock('@/utils/litegraphUtil', () => ({ + createNode: vi.fn(), isAudioNode: vi.fn(), isImageNode: vi.fn(), isVideoNode: vi.fn() @@ -99,34 +105,32 @@ describe('pasteImageNode', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(mockCanvas.graph!.add).mockImplementation( - (node: LGraphNode | LGraphGroup) => node as LGraphNode + (node: LGraphNode | LGraphGroup | null) => node as LGraphNode ) }) - it('should create new LoadImage node when no image node provided', () => { + it('should create new LoadImage node when no image node provided', async () => { const mockNode = createMockNode() - vi.mocked(LiteGraph.createNode).mockReturnValue( - mockNode as unknown as LGraphNode - ) + vi.mocked(createNode).mockResolvedValue(mockNode as unknown as LGraphNode) const file = createImageFile() const dataTransfer = createDataTransfer([file]) - pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items) + await pasteImageNode( + mockCanvas as unknown as LGraphCanvas, + dataTransfer.items + ) - expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') - expect(mockNode.pos).toEqual([100, 200]) - expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode) - expect(mockCanvas.graph!.change).toHaveBeenCalled() + expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadImage') expect(mockNode.pasteFile).toHaveBeenCalledWith(file) }) - it('should use existing image node when provided', () => { + it('should use existing image node when provided', async () => { const mockNode = createMockNode() const file = createImageFile() const dataTransfer = createDataTransfer([file]) - pasteImageNode( + await pasteImageNode( mockCanvas as unknown as LGraphCanvas, dataTransfer.items, mockNode as unknown as LGraphNode @@ -136,13 +140,13 @@ describe('pasteImageNode', () => { expect(mockNode.pasteFiles).toHaveBeenCalledWith([file]) }) - it('should handle multiple image files', () => { + it('should handle multiple image files', async () => { const mockNode = createMockNode() const file1 = createImageFile('test1.png') const file2 = createImageFile('test2.jpg', 'image/jpeg') const dataTransfer = createDataTransfer([file1, file2]) - pasteImageNode( + await pasteImageNode( mockCanvas as unknown as LGraphCanvas, dataTransfer.items, mockNode as unknown as LGraphNode @@ -152,11 +156,11 @@ describe('pasteImageNode', () => { expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2]) }) - it('should do nothing when no image files present', () => { + it('should do nothing when no image files present', async () => { const mockNode = createMockNode() const dataTransfer = createDataTransfer() - pasteImageNode( + await pasteImageNode( mockCanvas as unknown as LGraphCanvas, dataTransfer.items, mockNode as unknown as LGraphNode @@ -166,13 +170,13 @@ describe('pasteImageNode', () => { expect(mockNode.pasteFiles).not.toHaveBeenCalled() }) - it('should filter non-image items', () => { + it('should filter non-image items', async () => { const mockNode = createMockNode() const imageFile = createImageFile() const textFile = new File([''], 'test.txt', { type: 'text/plain' }) const dataTransfer = createDataTransfer([textFile, imageFile]) - pasteImageNode( + await pasteImageNode( mockCanvas as unknown as LGraphCanvas, dataTransfer.items, mockNode as unknown as LGraphNode @@ -183,21 +187,61 @@ describe('pasteImageNode', () => { }) }) +describe('pasteImageNodes', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should create multiple nodes for multiple files', async () => { + const mockNode1 = createMockNode() + const mockNode2 = createMockNode() + vi.mocked(createNode) + .mockResolvedValueOnce(mockNode1 as unknown as LGraphNode) + .mockResolvedValueOnce(mockNode2 as unknown as LGraphNode) + + const file1 = createImageFile('test1.png') + const file2 = createImageFile('test2.jpg', 'image/jpeg') + const fileList = createDataTransfer([file1, file2]).files + + const result = await pasteImageNodes( + mockCanvas as unknown as LGraphCanvas, + fileList + ) + + expect(createNode).toHaveBeenCalledTimes(2) + expect(createNode).toHaveBeenNthCalledWith(1, mockCanvas, 'LoadImage') + expect(createNode).toHaveBeenNthCalledWith(2, mockCanvas, 'LoadImage') + expect(mockNode1.pasteFile).toHaveBeenCalledWith(file1) + expect(mockNode2.pasteFile).toHaveBeenCalledWith(file2) + expect(result).toEqual([mockNode1, mockNode2]) + }) + + it('should handle empty file list', async () => { + const fileList = createDataTransfer([]).files + + const result = await pasteImageNodes( + mockCanvas as unknown as LGraphCanvas, + fileList + ) + + expect(createNode).not.toHaveBeenCalled() + expect(result).toEqual([]) + }) +}) + describe('usePaste', () => { beforeEach(() => { vi.clearAllMocks() mockCanvas.current_node = null mockWorkspaceStore.shiftDown = false vi.mocked(mockCanvas.graph!.add).mockImplementation( - (node: LGraphNode | LGraphGroup) => node as LGraphNode + (node: LGraphNode | LGraphGroup | null) => node as LGraphNode ) }) it('should handle image paste', async () => { const mockNode = createMockNode() - vi.mocked(LiteGraph.createNode).mockReturnValue( - mockNode as unknown as LGraphNode - ) + vi.mocked(createNode).mockResolvedValue(mockNode as unknown as LGraphNode) usePaste() @@ -207,7 +251,7 @@ describe('usePaste', () => { document.dispatchEvent(event) await vi.waitFor(() => { - expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') + expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadImage') expect(mockNode.pasteFile).toHaveBeenCalledWith(file) }) }) @@ -312,3 +356,62 @@ describe('usePaste', () => { }) }) }) + +describe('cloneDataTransfer', () => { + it('should clone string data', () => { + const original = new DataTransfer() + original.setData('text/plain', 'test text') + original.setData('text/html', '

test html

') + + const cloned = cloneDataTransfer(original) + + expect(cloned.getData('text/plain')).toBe('test text') + expect(cloned.getData('text/html')).toBe('

test html

') + }) + + it('should clone files', () => { + const file1 = createImageFile('test1.png') + const file2 = createImageFile('test2.jpg', 'image/jpeg') + const original = createDataTransfer([file1, file2]) + + const cloned = cloneDataTransfer(original) + + // Files are added from both .files and .items, causing duplicates + expect(cloned.files.length).toBeGreaterThanOrEqual(2) + expect(Array.from(cloned.files)).toContain(file1) + expect(Array.from(cloned.files)).toContain(file2) + }) + + it('should preserve dropEffect and effectAllowed', () => { + const original = new DataTransfer() + original.dropEffect = 'copy' + original.effectAllowed = 'copyMove' + + const cloned = cloneDataTransfer(original) + + expect(cloned.dropEffect).toBe('copy') + expect(cloned.effectAllowed).toBe('copyMove') + }) + + it('should handle empty DataTransfer', () => { + const original = new DataTransfer() + + const cloned = cloneDataTransfer(original) + + expect(cloned.types.length).toBe(0) + expect(cloned.files.length).toBe(0) + }) + + it('should clone both string data and files', () => { + const file = createImageFile() + const original = createDataTransfer([file]) + original.setData('text/plain', 'test') + + const cloned = cloneDataTransfer(original) + + expect(cloned.getData('text/plain')).toBe('test') + // Files are added from both .files and .items + expect(cloned.files.length).toBeGreaterThanOrEqual(1) + expect(Array.from(cloned.files)).toContain(file) + }) +}) diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index 1809eb838d7..0cdcb73af41 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -6,9 +6,42 @@ import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/w import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { app } from '@/scripts/app' import { useWorkspaceStore } from '@/stores/workspaceStore' -import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil' +import { createNode, isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil' import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers' +export function cloneDataTransfer(original: DataTransfer): DataTransfer { + const persistent = new DataTransfer() + + // Copy string data + for (const type of original.types) { + const data = original.getData(type) + if (data) { + persistent.setData(type, data) + } + } + + // Copy files + for (const file of original.files) { + persistent.items.add(file) + } + + // Also handle any file-kind items that might not be in .files + for (const item of original.items) { + if (item.kind === 'file') { + const file = item.getAsFile() + if (file) { + persistent.items.add(file) + } + } + } + + // Preserve dropEffect and effectAllowed + persistent.dropEffect = original.dropEffect + persistent.effectAllowed = original.effectAllowed + + return persistent +} + function pasteClipboardItems(data: DataTransfer): boolean { const rawData = data.getData('text/html') const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1] @@ -48,27 +81,37 @@ function pasteItemsOnNode( ) } -export function pasteImageNode( +export async function pasteImageNode( canvas: LGraphCanvas, items: DataTransferItemList, imageNode: LGraphNode | null = null -): void { - const { - graph, - graph_mouse: [posX, posY] - } = canvas - +): Promise { + // No image node selected: add a new one if (!imageNode) { - // No image node selected: add a new one - const newNode = LiteGraph.createNode('LoadImage') - if (newNode) { - newNode.pos = [posX, posY] - imageNode = graph?.add(newNode) ?? null - } - graph?.change() + imageNode = await createNode(canvas, 'LoadImage') } pasteItemsOnNode(items, imageNode, 'image') + return imageNode +} + +export async function pasteImageNodes( + canvas: LGraphCanvas, + fileList: FileList +): Promise { + const nodes: LGraphNode[] = [] + + for (const file of fileList) { + const transfer = new DataTransfer() + transfer.items.add(file) + const imageNode = await pasteImageNode(canvas, transfer.items) + + if (imageNode) { + nodes.push(imageNode) + } + } + + return nodes } /** @@ -93,6 +136,7 @@ export const usePaste = () => { const { graph } = canvas let data: DataTransfer | string | null = e.clipboardData if (!data) throw new Error('No clipboard data on clipboard event') + data = cloneDataTransfer(data) const { items } = data @@ -114,7 +158,7 @@ export const usePaste = () => { // Look for image paste data for (const item of items) { if (item.type.startsWith('image/')) { - pasteImageNode(canvas as LGraphCanvas, items, imageNode) + await pasteImageNode(canvas as LGraphCanvas, items, imageNode) return } else if (item.type.startsWith('video/')) { if (!videoNode) { diff --git a/src/lib/litegraph/src/LGraph.test.ts b/src/lib/litegraph/src/LGraph.test.ts index 1c2f38da287..e21543aad30 100644 --- a/src/lib/litegraph/src/LGraph.test.ts +++ b/src/lib/litegraph/src/LGraph.test.ts @@ -38,6 +38,17 @@ describe('LGraph', () => { expect(result1).toEqual(result2) }) + + it('should handle adding null node gracefully', () => { + const graph = new LGraph() + const initialNodeCount = graph.nodes.length + + const result = graph.add(null) + + expect(result).toBeUndefined() + expect(graph.nodes.length).toBe(initialNodeCount) + }) + test('can be instantiated', ({ expect }) => { // @ts-expect-error Intentional - extra holds any / all consumer data that should be serialised const graph = new LGraph({ extra: 'TestGraph' }) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 49ad351010a..b4fe6db8a6c 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -862,9 +862,10 @@ export class LGraph /** * Adds a new node instance to this graph * @param node the instance of the node + * @param skip_compute_order */ add( - node: LGraphNode | LGraphGroup, + node: LGraphNode | LGraphGroup | null, skip_compute_order?: boolean ): LGraphNode | null | undefined { if (!node) return diff --git a/src/scripts/app.test.ts b/src/scripts/app.test.ts new file mode 100644 index 00000000000..680c7020125 --- /dev/null +++ b/src/scripts/app.test.ts @@ -0,0 +1,271 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { ComfyApp } from './app' +import { createNode } from '@/utils/litegraphUtil' +import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste' + +vi.mock('@/utils/litegraphUtil', () => ({ + createNode: vi.fn(), + isImageNode: vi.fn(), + isVideoNode: vi.fn(), + isAudioNode: vi.fn(), + executeWidgetsCallback: vi.fn(), + fixLinkInputSlots: vi.fn() +})) + +vi.mock('@/composables/usePaste', () => ({ + pasteImageNode: vi.fn(), + pasteImageNodes: vi.fn() +})) + +vi.mock('@/scripts/metadata/parser', () => ({ + getWorkflowDataFromFile: vi.fn() +})) + +vi.mock('@/platform/updates/common/toastStore', () => ({ + useToastStore: vi.fn(() => ({ + addAlert: vi.fn(), + add: vi.fn(), + remove: vi.fn() + })) +})) + +function createMockNode(options: any = {}): LGraphNode { + return { + id: 1, + pos: [0, 0], + size: [200, 100], + type: 'LoadImage', + connect: vi.fn(), + getBounding: vi.fn(() => new Float64Array([0, 0, 200, 100])), + ...options + } as unknown as LGraphNode +} + +function createMockCanvas(): Partial { + return { + graph: { + change: vi.fn() + } as any, + selectItems: vi.fn() + } +} + +function createImageFile( + name: string = 'test.png', + type: string = 'image/png' +): File { + return new File([''], name, { type }) +} + +describe('ComfyApp', () => { + let app: ComfyApp + let mockCanvas: Partial + + beforeEach(() => { + vi.clearAllMocks() + app = new ComfyApp() + mockCanvas = createMockCanvas() + app.canvas = mockCanvas as LGraphCanvas + }) + + describe('handleFileList', () => { + it('should create image nodes for each file in the list', async () => { + const mockNode1 = createMockNode({ id: 1 }) + const mockNode2 = createMockNode({ id: 2 }) + const mockBatchNode = createMockNode({ id: 3, type: 'BatchImagesNode' }) + + vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1, mockNode2]) + vi.mocked(createNode).mockResolvedValue(mockBatchNode) + + const file1 = createImageFile('test1.png') + const file2 = createImageFile('test2.jpg', 'image/jpeg') + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file1) + dataTransfer.items.add(file2) + + await app.handleFileList(dataTransfer.files) + + expect(pasteImageNodes).toHaveBeenCalledWith( + mockCanvas, + dataTransfer.files + ) + expect(createNode).toHaveBeenCalledWith(mockCanvas, 'BatchImagesNode') + expect(mockCanvas.selectItems).toHaveBeenCalledWith([ + mockNode1, + mockNode2, + mockBatchNode + ]) + expect(mockNode1.connect).toHaveBeenCalledWith(0, mockBatchNode, 0) + expect(mockNode2.connect).toHaveBeenCalledWith(0, mockBatchNode, 1) + }) + + it('should not proceed if batch node creation fails', async () => { + const mockNode1 = createMockNode({ id: 1 }) + vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1]) + vi.mocked(createNode).mockResolvedValue(null) + + const file = createImageFile() + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + await app.handleFileList(dataTransfer.files) + + expect(mockCanvas.selectItems).not.toHaveBeenCalled() + expect(mockNode1.connect).not.toHaveBeenCalled() + }) + + it('should handle empty file list', async () => { + const dataTransfer = new DataTransfer() + + // The implementation doesn't check for empty list and will throw + await expect(app.handleFileList(dataTransfer.files)).rejects.toThrow() + }) + + it('should not process non-image files', async () => { + const textFile = new File([''], 'test.txt', { type: 'text/plain' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(textFile) + + await app.handleFileList(dataTransfer.files) + + expect(pasteImageNodes).not.toHaveBeenCalled() + expect(createNode).not.toHaveBeenCalled() + }) + }) + + describe('positionBatchNodes', () => { + it('should position batch node to the right of first node', () => { + const mockNode1 = createMockNode({ + pos: [100, 200], + getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) + }) + const mockBatchNode = createMockNode({ pos: [0, 0] }) + + app.positionBatchNodes([mockNode1], mockBatchNode) + + expect(mockBatchNode.pos).toEqual([500, 230]) + }) + + it('should stack multiple nodes vertically', () => { + const mockNode1 = createMockNode({ + pos: [100, 200], + type: 'LoadImage', + getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) + }) + const mockNode2 = createMockNode({ + pos: [0, 0], + type: 'LoadImage' + }) + const mockNode3 = createMockNode({ + pos: [0, 0], + type: 'LoadImage' + }) + const mockBatchNode = createMockNode({ pos: [0, 0] }) + + app.positionBatchNodes([mockNode1, mockNode2, mockNode3], mockBatchNode) + + // Formula: y + (height * index) + (25 * (index + 1)) + // For LoadImage nodes, height = max(344, nodeHeight) = max(344, 400) = 400 + expect(mockNode1.pos).toEqual([100, 200]) + // index 1: 200 + (400 * 1) + (25 * 2) = 200 + 400 + 50 = 650 + expect(mockNode2.pos).toEqual([100, 650]) + // index 2: 200 + (400 * 2) + (25 * 3) = 200 + 800 + 75 = 1075 + expect(mockNode3.pos).toEqual([100, 1075]) + }) + + it('should use minimum height for LoadImage nodes', () => { + const mockNode1 = createMockNode({ + pos: [100, 200], + type: 'LoadImage', + getBounding: vi.fn(() => new Float64Array([100, 200, 300, 100])) + }) + const mockNode2 = createMockNode({ + pos: [0, 0], + type: 'LoadImage' + }) + const mockBatchNode = createMockNode({ pos: [0, 0] }) + + app.positionBatchNodes([mockNode1, mockNode2], mockBatchNode) + + // height = max(344, 100) = 344 + // index 1: 200 + (344 * 1) + (25 * 2) = 200 + 344 + 50 = 594 + expect(mockNode2.pos).toEqual([100, 594]) + }) + + it('should use actual height for non-LoadImage nodes', () => { + const mockNode1 = createMockNode({ + pos: [100, 200], + type: 'SomeOtherNode', + getBounding: vi.fn(() => new Float64Array([100, 200, 300, 150])) + }) + const mockNode2 = createMockNode({ + pos: [0, 0], + type: 'SomeOtherNode' + }) + const mockBatchNode = createMockNode({ pos: [0, 0] }) + + app.positionBatchNodes([mockNode1, mockNode2], mockBatchNode) + + // height = 150 (no max() for non-LoadImage) + // index 1: 200 + (150 * 1) + (25 * 2) = 200 + 150 + 50 = 400 + expect(mockNode2.pos).toEqual([100, 400]) + }) + + it('should call graph change for each node', () => { + const mockNode1 = createMockNode({ + getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) + }) + const mockNode2 = createMockNode() + const mockNode3 = createMockNode() + const mockBatchNode = createMockNode() + + app.positionBatchNodes([mockNode1, mockNode2, mockNode3], mockBatchNode) + + // graph.change() is called for each node in the forEach + expect(mockCanvas.graph?.change).toHaveBeenCalledTimes(3) + }) + }) + + describe('handleFile', () => { + it('should handle image files by creating LoadImage node', async () => { + const { getWorkflowDataFromFile } = + await import('@/scripts/metadata/parser') + vi.mocked(getWorkflowDataFromFile).mockResolvedValue({}) + + const mockNode = createMockNode() + vi.mocked(createNode).mockResolvedValue(mockNode) + + const imageFile = createImageFile() + + await app.handleFile(imageFile) + + expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadImage') + expect(pasteImageNode).toHaveBeenCalledWith( + mockCanvas, + expect.any(DataTransferItemList), + mockNode + ) + }) + + it('should show error toast for unsupported files', async () => { + const { getWorkflowDataFromFile } = + await import('@/scripts/metadata/parser') + const { useToastStore } = + await import('@/platform/updates/common/toastStore') + const mockAddAlert = vi.fn() + + vi.mocked(getWorkflowDataFromFile).mockResolvedValue({}) + vi.mocked(useToastStore).mockReturnValue({ + addAlert: mockAddAlert + } as any) + + const textFile = new File([''], 'test.txt', { type: 'text/plain' }) + + await app.handleFile(textFile) + + expect(mockAddAlert).toHaveBeenCalled() + }) + }) +}) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 0a794ab9db3..c3adae56895 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -77,6 +77,7 @@ import { } from '@/utils/graphTraversalUtil' import { executeWidgetsCallback, + createNode, fixLinkInputSlots, isImageNode } from '@/utils/litegraphUtil' @@ -97,7 +98,7 @@ import { type ComfyWidgetConstructor } from './widgets' import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale' import { extractFileFromDragEvent } from '@/utils/eventUtils' import { getWorkflowDataFromFile } from '@/scripts/metadata/parser' -import { pasteImageNode } from '@/composables/usePaste' +import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste' export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview' @@ -542,7 +543,13 @@ export class ComfyApp { const workspace = useWorkspaceStore() try { workspace.spinner = true - await this.handleFile(fileMaybe, 'file_drop') + if (fileMaybe instanceof File) { + await this.handleFile(fileMaybe, 'file_drop') + } + + if (fileMaybe instanceof FileList) { + await this.handleFileList(fileMaybe) + } } finally { workspace.spinner = false } @@ -1437,7 +1444,8 @@ export class ComfyApp { if (file.type.startsWith('image')) { const transfer = new DataTransfer() transfer.items.add(file) - pasteImageNode(this.canvas, transfer.items) + const imageNode = await createNode(this.canvas, 'LoadImage') + await pasteImageNode(this.canvas, transfer.items, imageNode) return } @@ -1516,6 +1524,49 @@ export class ComfyApp { this.showErrorOnFileLoad(file) } + + /** + * Loads multiple files, connects to a batch node, and selects them + * @param {FileList} fileList + */ + async handleFileList(fileList: FileList) { + if (fileList[0].type.startsWith('image')) { + const imageNodes = await pasteImageNodes(this.canvas, fileList) + const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode') + if (!batchImagesNode) return + + this.positionBatchNodes(imageNodes, batchImagesNode) + this.canvas.selectItems([...imageNodes, batchImagesNode]) + + Array.from(imageNodes).forEach((imageNode, index) => { + imageNode.connect(0, batchImagesNode, index) + }) + } + } + + /** + * Positions batched nodes in drag and drop + * @param nodes + * @param batchNode + */ + positionBatchNodes(nodes: LGraphNode[], batchNode: LGraphNode): void { + const [x, y, width, nodeHeight] = nodes[0].getBounding() + batchNode.pos = [ x + width + 100, y + 30 ] + + // Retrieving Node Height is inconsistent + let height = nodeHeight; + if (nodes[0].type === 'LoadImage') { + height = Math.max(344, nodeHeight) + } + + nodes.forEach((node, index) => { + if (index > 0) { + node.pos = [ x, y + (height * index) + (25 * (index + 1)) ] + } + this.canvas.graph?.change() + }); + } + // @deprecated isApiJson(data: unknown): data is ComfyApiWorkflow { if (!_.isObject(data) || Array.isArray(data)) { diff --git a/src/utils/__tests__/eventUtils.test.ts b/src/utils/__tests__/eventUtils.test.ts index 2fc51ac677a..ca20da96c91 100644 --- a/src/utils/__tests__/eventUtils.test.ts +++ b/src/utils/__tests__/eventUtils.test.ts @@ -33,6 +33,45 @@ describe('eventUtils', () => { expect(actual).toBe(fileWithWorkflowMaybeWhoKnows) }) + it('should handle drops with multiple image files', async () => { + const imageFile1 = new File([new Uint8Array()], 'image1.png', { + type: 'image/png' + }) + const imageFile2 = new File([new Uint8Array()], 'image2.jpg', { + type: 'image/jpeg' + }) + + const dataTransfer = new DataTransfer() + dataTransfer.items.add(imageFile1) + dataTransfer.items.add(imageFile2) + + const event = new FakeDragEvent('drop', { dataTransfer }) + + const actual = await extractFileFromDragEvent(event) + expect(actual).toBeDefined() + expect((actual as FileList).length).toBe(2) + expect((actual as FileList)[0]).toBe(imageFile1) + expect((actual as FileList)[1]).toBe(imageFile2) + }) + + it('should return undefined when dropping multiple non-image files', async () => { + const file1 = new File([new Uint8Array()], 'file1.txt', { + type: 'text/plain' + }) + const file2 = new File([new Uint8Array()], 'file2.txt', { + type: 'text/plain' + }) + + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file1) + dataTransfer.items.add(file2) + + const event = new FakeDragEvent('drop', { dataTransfer }) + + const actual = await extractFileFromDragEvent(event) + expect(actual).toBe(undefined) + }) + // Skip until we can setup MSW it.skip('should handle drops with URLs', async () => { const urlWithWorkflow = 'https://fakewebsite.notreal/fake_workflow.json' diff --git a/src/utils/eventUtils.ts b/src/utils/eventUtils.ts index 133ccd709bd..63a0af8963a 100644 --- a/src/utils/eventUtils.ts +++ b/src/utils/eventUtils.ts @@ -1,14 +1,14 @@ export async function extractFileFromDragEvent( event: DragEvent -): Promise { +): Promise { if (!event.dataTransfer) return - // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that - if ( - event.dataTransfer.files.length && - event.dataTransfer.files[0].type !== 'image/bmp' - ) { - return event.dataTransfer.files[0] + const { files } = event.dataTransfer + // Dragging from Chrome->Firefox there is a file, but it's a bmp, so ignore it + if (files.length === 1 && files[0].type !== 'image/bmp') { + return files[0] + } else if (files.length > 1 && Array.from(files).every(hasImageType)) { + return files } // Try loading the first URI in the transfer list @@ -25,3 +25,5 @@ export async function extractFileFromDragEvent( const blob = await response.blob() return new File([blob], uri, { type: blob.type }) } + +const hasImageType = ({ type }: File): Boolean => type.startsWith('image'); diff --git a/src/utils/litegraphUtil.test.ts b/src/utils/litegraphUtil.test.ts index 36a6692ee92..54fdb5c0002 100644 --- a/src/utils/litegraphUtil.test.ts +++ b/src/utils/litegraphUtil.test.ts @@ -1,13 +1,125 @@ -import { describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation' import type { IWidget } from '@/lib/litegraph/src/types/widgets' import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import { compressWidgetInputSlots, + createNode, migrateWidgetsValues } from '@/utils/litegraphUtil' +vi.mock('@/lib/litegraph/src/litegraph', () => ({ + LiteGraph: { + createNode: vi.fn() + } +})) + +vi.mock('@/platform/updates/common/toastStore', () => ({ + useToastStore: vi.fn(() => ({ + addAlert: vi.fn() + })) +})) + +vi.mock('@/i18n', () => ({ + t: vi.fn((key) => key) +})) + +describe('createNode', () => { + let mockCanvas: Partial + let mockGraph: any + + beforeEach(() => { + vi.clearAllMocks() + mockGraph = { + add: vi.fn((node) => node), + change: vi.fn() + } + mockCanvas = { + graph: mockGraph, + graph_mouse: [100, 200] + } + }) + + it('should create a node successfully', async () => { + const mockNode = { + pos: [0, 0] + } as unknown as LGraphNode + + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode) + + const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') + + expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') + expect(mockNode.pos).toEqual([100, 200]) + expect(mockGraph.add).toHaveBeenCalledWith(mockNode) + expect(mockGraph.change).toHaveBeenCalled() + expect(result).toBe(mockNode) + }) + + it('should return null when name is empty', async () => { + const result = await createNode(mockCanvas as LGraphCanvas, '') + + expect(LiteGraph.createNode).not.toHaveBeenCalled() + expect(result).toBeNull() + }) + + it('should return null when name is falsy', async () => { + const result = await createNode(mockCanvas as LGraphCanvas, null as any) + + expect(LiteGraph.createNode).not.toHaveBeenCalled() + expect(result).toBeNull() + }) + + it('should handle node creation failure and show toast', async () => { + const { useToastStore } = + await import('@/platform/updates/common/toastStore') + const mockAddAlert = vi.fn() + vi.mocked(useToastStore).mockReturnValue({ addAlert: mockAddAlert } as any) + + vi.mocked(LiteGraph.createNode).mockReturnValue(null) + + const result = await createNode(mockCanvas as LGraphCanvas, 'InvalidNode') + + expect(mockAddAlert).toHaveBeenCalledWith('assetBrowser.failedToCreateNode') + expect(result).toBeNull() + }) + + it('should handle graph being null', async () => { + const mockNode = { + pos: [0, 0] + } as unknown as LGraphNode + + mockCanvas.graph = null + + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode) + + const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') + + expect(mockNode.pos).toEqual([100, 200]) + expect(result).toBeNull() + }) + + it('should set position based on canvas graph_mouse', async () => { + const mockCanvasWithDifferentPos = { + ...mockCanvas, + graph_mouse: [250, 350] + } + + const mockNode = { + pos: [0, 0] + } as unknown as LGraphNode + + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode) + + await createNode(mockCanvasWithDifferentPos as LGraphCanvas, 'LoadAudio') + + expect(mockNode.pos).toEqual([250, 350]) + }) +}) + describe('migrateWidgetsValues', () => { it('should remove widget values for forceInput inputs', () => { const inputDefs: Record = { diff --git a/src/utils/litegraphUtil.ts b/src/utils/litegraphUtil.ts index 898b3ef237f..1f31637b232 100644 --- a/src/utils/litegraphUtil.ts +++ b/src/utils/litegraphUtil.ts @@ -1,6 +1,11 @@ import _ from 'es-toolkit/compat' -import type { ColorOption, LGraph } from '@/lib/litegraph/src/litegraph' +import type { + ColorOption, + LGraph, + LGraphCanvas, +} from '@/lib/litegraph/src/litegraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LGraphGroup, LGraphNode, @@ -17,6 +22,8 @@ import type { IComboWidget } from '@/lib/litegraph/src/types/widgets' import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' +import { useToastStore } from '@/platform/updates/common/toastStore' +import { t } from '@/i18n' type ImageNode = LGraphNode & { imgs: HTMLImageElement[] | undefined } type VideoNode = LGraphNode & { @@ -24,6 +31,34 @@ type VideoNode = LGraphNode & { imgs: HTMLVideoElement[] | undefined } +/** + * Extract & Promisify Litegraph.createNode to allow for positioning + * @param canvas + * @param name + */ +export async function createNode( + canvas: LGraphCanvas, + name: string +): Promise { + if (!name) { + return null + } + + const { graph, graph_mouse: [ posX, posY ] } = canvas + const newNode = LiteGraph.createNode(name) + await new Promise(r => setTimeout(r, 0)) + + if (newNode) { + newNode!.pos = [ posX, posY ] + const addedNode = graph?.add(newNode!) ?? null + graph?.change() + return addedNode + } else { + useToastStore().addAlert(t('assetBrowser.failedToCreateNode')) + return null + } +} + export function isImageNode(node: LGraphNode | undefined): node is ImageNode { if (!node) return false return ( From 44c4ebcc06972a48dc89986e427ab99593823f43 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 23 Jan 2026 15:11:45 -0600 Subject: [PATCH 02/15] Use items only, files is just read only, added comment --- src/composables/usePaste.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index 0cdcb73af41..9f752823717 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -10,36 +10,31 @@ import { createNode, isAudioNode, isImageNode, isVideoNode } from '@/utils/liteg import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers' export function cloneDataTransfer(original: DataTransfer): DataTransfer { - const persistent = new DataTransfer() + const persistent = new DataTransfer(); // Copy string data for (const type of original.types) { - const data = original.getData(type) + const data = original.getData(type); if (data) { - persistent.setData(type, data) + persistent.setData(type, data); } } - // Copy files - for (const file of original.files) { - persistent.items.add(file) - } - - // Also handle any file-kind items that might not be in .files + // Copy files (items.add() is idempotent - won't create duplicates) for (const item of original.items) { if (item.kind === 'file') { - const file = item.getAsFile() + const file = item.getAsFile(); if (file) { - persistent.items.add(file) + persistent.items.add(file); } } } // Preserve dropEffect and effectAllowed - persistent.dropEffect = original.dropEffect - persistent.effectAllowed = original.effectAllowed + persistent.dropEffect = original.dropEffect; + persistent.effectAllowed = original.effectAllowed; - return persistent + return persistent; } function pasteClipboardItems(data: DataTransfer): boolean { From 2fc43055e381e7efb9195b800c3c11001f1d819a Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 23 Jan 2026 15:17:57 -0600 Subject: [PATCH 03/15] Change to function signature --- src/utils/eventUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/eventUtils.ts b/src/utils/eventUtils.ts index 63a0af8963a..a9a789c723a 100644 --- a/src/utils/eventUtils.ts +++ b/src/utils/eventUtils.ts @@ -26,4 +26,6 @@ export async function extractFileFromDragEvent( return new File([blob], uri, { type: blob.type }) } -const hasImageType = ({ type }: File): Boolean => type.startsWith('image'); +function hasImageType({ type }: File): boolean { + return type.startsWith('image') +} From 790432038c8cfc7e87c26648ba03d2451b8de622 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 23 Jan 2026 15:35:57 -0600 Subject: [PATCH 04/15] Remove comment --- src/composables/usePaste.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index 9f752823717..2cd08983327 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -20,7 +20,6 @@ export function cloneDataTransfer(original: DataTransfer): DataTransfer { } } - // Copy files (items.add() is idempotent - won't create duplicates) for (const item of original.items) { if (item.kind === 'file') { const file = item.getAsFile(); From bd6df613af7856397e4967a640a19d1a5828efa0 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 23 Jan 2026 17:25:06 -0600 Subject: [PATCH 05/15] Updating types --- src/composables/usePaste.ts | 23 +++++++++++++--------- src/scripts/app.test.ts | 18 +++++++++++------ src/utils/litegraphUtil.test.ts | 35 ++++++++++++++++----------------- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index 2cd08983327..75d5e3af41e 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -6,34 +6,39 @@ import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/w import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { app } from '@/scripts/app' import { useWorkspaceStore } from '@/stores/workspaceStore' -import { createNode, isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil' +import { + createNode, + isAudioNode, + isImageNode, + isVideoNode +} from '@/utils/litegraphUtil' import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers' export function cloneDataTransfer(original: DataTransfer): DataTransfer { - const persistent = new DataTransfer(); + const persistent = new DataTransfer() // Copy string data for (const type of original.types) { - const data = original.getData(type); + const data = original.getData(type) if (data) { - persistent.setData(type, data); + persistent.setData(type, data) } } for (const item of original.items) { if (item.kind === 'file') { - const file = item.getAsFile(); + const file = item.getAsFile() if (file) { - persistent.items.add(file); + persistent.items.add(file) } } } // Preserve dropEffect and effectAllowed - persistent.dropEffect = original.dropEffect; - persistent.effectAllowed = original.effectAllowed; + persistent.dropEffect = original.dropEffect + persistent.effectAllowed = original.effectAllowed - return persistent; + return persistent } function pasteClipboardItems(data: DataTransfer): boolean { diff --git a/src/scripts/app.test.ts b/src/scripts/app.test.ts index 680c7020125..04cd306bd1e 100644 --- a/src/scripts/app.test.ts +++ b/src/scripts/app.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { + LGraph, + LGraphCanvas, + LGraphNode +} from '@/lib/litegraph/src/litegraph' import { ComfyApp } from './app' import { createNode } from '@/utils/litegraphUtil' import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste' @@ -31,7 +35,7 @@ vi.mock('@/platform/updates/common/toastStore', () => ({ })) })) -function createMockNode(options: any = {}): LGraphNode { +function createMockNode(options: Record = {}): LGraphNode { return { id: 1, pos: [0, 0], @@ -44,10 +48,12 @@ function createMockNode(options: any = {}): LGraphNode { } function createMockCanvas(): Partial { + const mockGraph: Partial = { + change: vi.fn() + } + return { - graph: { - change: vi.fn() - } as any, + graph: mockGraph as LGraph, selectItems: vi.fn() } } @@ -259,7 +265,7 @@ describe('ComfyApp', () => { vi.mocked(getWorkflowDataFromFile).mockResolvedValue({}) vi.mocked(useToastStore).mockReturnValue({ addAlert: mockAddAlert - } as any) + } as unknown as ReturnType) const textFile = new File([''], 'test.txt', { type: 'text/plain' }) diff --git a/src/utils/litegraphUtil.test.ts b/src/utils/litegraphUtil.test.ts index 54fdb5c0002..0ee0530c4a3 100644 --- a/src/utils/litegraphUtil.test.ts +++ b/src/utils/litegraphUtil.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { + LGraph, + LGraphCanvas, + LGraphNode +} from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation' import type { IWidget } from '@/lib/litegraph/src/types/widgets' @@ -29,7 +33,7 @@ vi.mock('@/i18n', () => ({ describe('createNode', () => { let mockCanvas: Partial - let mockGraph: any + let mockGraph: Partial beforeEach(() => { vi.clearAllMocks() @@ -38,7 +42,7 @@ describe('createNode', () => { change: vi.fn() } mockCanvas = { - graph: mockGraph, + graph: mockGraph as LGraph, graph_mouse: [100, 200] } }) @@ -46,9 +50,9 @@ describe('createNode', () => { it('should create a node successfully', async () => { const mockNode = { pos: [0, 0] - } as unknown as LGraphNode + } as Partial - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode) + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') @@ -66,18 +70,13 @@ describe('createNode', () => { expect(result).toBeNull() }) - it('should return null when name is falsy', async () => { - const result = await createNode(mockCanvas as LGraphCanvas, null as any) - - expect(LiteGraph.createNode).not.toHaveBeenCalled() - expect(result).toBeNull() - }) - it('should handle node creation failure and show toast', async () => { const { useToastStore } = await import('@/platform/updates/common/toastStore') const mockAddAlert = vi.fn() - vi.mocked(useToastStore).mockReturnValue({ addAlert: mockAddAlert } as any) + vi.mocked(useToastStore).mockReturnValue({ + addAlert: mockAddAlert + } as unknown as ReturnType) vi.mocked(LiteGraph.createNode).mockReturnValue(null) @@ -90,11 +89,11 @@ describe('createNode', () => { it('should handle graph being null', async () => { const mockNode = { pos: [0, 0] - } as unknown as LGraphNode + } as Partial mockCanvas.graph = null - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode) + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') @@ -103,16 +102,16 @@ describe('createNode', () => { }) it('should set position based on canvas graph_mouse', async () => { - const mockCanvasWithDifferentPos = { + const mockCanvasWithDifferentPos: Partial = { ...mockCanvas, graph_mouse: [250, 350] } const mockNode = { pos: [0, 0] - } as unknown as LGraphNode + } as Partial - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode) + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) await createNode(mockCanvasWithDifferentPos as LGraphCanvas, 'LoadAudio') From 7c0088839879332f283b0c02b4e9cf82747d049e Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 23 Jan 2026 19:28:24 -0600 Subject: [PATCH 06/15] Call graph.change() once --- src/scripts/app.test.ts | 4 ++-- src/scripts/app.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scripts/app.test.ts b/src/scripts/app.test.ts index 04cd306bd1e..244cd10b1b7 100644 --- a/src/scripts/app.test.ts +++ b/src/scripts/app.test.ts @@ -219,7 +219,7 @@ describe('ComfyApp', () => { expect(mockNode2.pos).toEqual([100, 400]) }) - it('should call graph change for each node', () => { + it('should call graph change once for all nodes', () => { const mockNode1 = createMockNode({ getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) }) @@ -230,7 +230,7 @@ describe('ComfyApp', () => { app.positionBatchNodes([mockNode1, mockNode2, mockNode3], mockBatchNode) // graph.change() is called for each node in the forEach - expect(mockCanvas.graph?.change).toHaveBeenCalledTimes(3) + expect(mockCanvas.graph?.change).toHaveBeenCalledTimes(1) }) }) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index c3adae56895..d35cbe63a50 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1563,8 +1563,9 @@ export class ComfyApp { if (index > 0) { node.pos = [ x, y + (height * index) + (25 * (index + 1)) ] } - this.canvas.graph?.change() }); + + this.canvas.graph?.change() } // @deprecated From 55634e4734b3af4251c7d607d8f00310a418fc5d Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 15:26:10 -0600 Subject: [PATCH 07/15] Don't use height form getBounding() --- src/scripts/app.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 687eba4bf67..7f445056806 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1564,13 +1564,13 @@ export class ComfyApp { * @param batchNode */ positionBatchNodes(nodes: LGraphNode[], batchNode: LGraphNode): void { - const [x, y, width, nodeHeight] = nodes[0].getBounding() + const [x, y, width] = nodes[0].getBounding() batchNode.pos = [ x + width + 100, y + 30 ] // Retrieving Node Height is inconsistent - let height = nodeHeight; + let height = 0; if (nodes[0].type === 'LoadImage') { - height = Math.max(344, nodeHeight) + height = 344 } nodes.forEach((node, index) => { From 7f7f3b8c25f067cf84db9c53143e089a01e8ac67 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 15:38:01 -0600 Subject: [PATCH 08/15] Update tests for positionBatchNodes --- src/scripts/app.test.ts | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/scripts/app.test.ts b/src/scripts/app.test.ts index 244cd10b1b7..b5e395ea55e 100644 --- a/src/scripts/app.test.ts +++ b/src/scripts/app.test.ts @@ -154,7 +154,7 @@ describe('ComfyApp', () => { expect(mockBatchNode.pos).toEqual([500, 230]) }) - it('should stack multiple nodes vertically', () => { + it('should stack multiple image nodes vertically', () => { const mockNode1 = createMockNode({ pos: [100, 200], type: 'LoadImage', @@ -173,15 +173,15 @@ describe('ComfyApp', () => { app.positionBatchNodes([mockNode1, mockNode2, mockNode3], mockBatchNode) // Formula: y + (height * index) + (25 * (index + 1)) - // For LoadImage nodes, height = max(344, nodeHeight) = max(344, 400) = 400 + // For LoadImage nodes, height = 344 expect(mockNode1.pos).toEqual([100, 200]) - // index 1: 200 + (400 * 1) + (25 * 2) = 200 + 400 + 50 = 650 - expect(mockNode2.pos).toEqual([100, 650]) - // index 2: 200 + (400 * 2) + (25 * 3) = 200 + 800 + 75 = 1075 - expect(mockNode3.pos).toEqual([100, 1075]) + // index 1: 200 + (344 * 1) + (25 * 2) = 200 + 344 + 50 = 594 + expect(mockNode2.pos).toEqual([100, 594]) + // index 2: 200 + (344 * 2) + (25 * 3) = 200 + 688 + 75 = 963 + expect(mockNode3.pos).toEqual([100, 963]) }) - it('should use minimum height for LoadImage nodes', () => { + it('should use set height of 344 for LoadImage nodes', () => { const mockNode1 = createMockNode({ pos: [100, 200], type: 'LoadImage', @@ -200,25 +200,6 @@ describe('ComfyApp', () => { expect(mockNode2.pos).toEqual([100, 594]) }) - it('should use actual height for non-LoadImage nodes', () => { - const mockNode1 = createMockNode({ - pos: [100, 200], - type: 'SomeOtherNode', - getBounding: vi.fn(() => new Float64Array([100, 200, 300, 150])) - }) - const mockNode2 = createMockNode({ - pos: [0, 0], - type: 'SomeOtherNode' - }) - const mockBatchNode = createMockNode({ pos: [0, 0] }) - - app.positionBatchNodes([mockNode1, mockNode2], mockBatchNode) - - // height = 150 (no max() for non-LoadImage) - // index 1: 200 + (150 * 1) + (25 * 2) = 200 + 150 + 50 = 400 - expect(mockNode2.pos).toEqual([100, 400]) - }) - it('should call graph change once for all nodes', () => { const mockNode1 = createMockNode({ getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) From daed3cb26e19648fa5d11f09f2f392ec65e540fe Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 16:29:38 -0600 Subject: [PATCH 09/15] Update logic / typescript with test --- src/utils/litegraphUtil.test.ts | 2 +- src/utils/litegraphUtil.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/litegraphUtil.test.ts b/src/utils/litegraphUtil.test.ts index 2460e9ba208..1de11395c0c 100644 --- a/src/utils/litegraphUtil.test.ts +++ b/src/utils/litegraphUtil.test.ts @@ -97,7 +97,7 @@ describe('createNode', () => { const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') - expect(mockNode.pos).toEqual([100, 200]) + expect(mockNode.pos).toEqual([0, 0]) expect(result).toBeNull() }) diff --git a/src/utils/litegraphUtil.ts b/src/utils/litegraphUtil.ts index 1f31637b232..a4d4d8ed11e 100644 --- a/src/utils/litegraphUtil.ts +++ b/src/utils/litegraphUtil.ts @@ -48,10 +48,10 @@ export async function createNode( const newNode = LiteGraph.createNode(name) await new Promise(r => setTimeout(r, 0)) - if (newNode) { - newNode!.pos = [ posX, posY ] - const addedNode = graph?.add(newNode!) ?? null - graph?.change() + if (newNode && graph) { + newNode.pos = [ posX, posY ] + const addedNode = graph.add(newNode) ?? null + graph.change() return addedNode } else { useToastStore().addAlert(t('assetBrowser.failedToCreateNode')) From 373af1390f5226ba395050f990472d7b6f920550 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 17:57:12 -0600 Subject: [PATCH 10/15] Update to add additional check --- src/utils/litegraphUtil.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/litegraphUtil.ts b/src/utils/litegraphUtil.ts index a4d4d8ed11e..bd526c065e0 100644 --- a/src/utils/litegraphUtil.ts +++ b/src/utils/litegraphUtil.ts @@ -51,7 +51,8 @@ export async function createNode( if (newNode && graph) { newNode.pos = [ posX, posY ] const addedNode = graph.add(newNode) ?? null - graph.change() + + if (addedNode) graph.change() return addedNode } else { useToastStore().addAlert(t('assetBrowser.failedToCreateNode')) From 758ed366c85aa89e835b3cb6148031e080b4f044 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 22:24:06 -0600 Subject: [PATCH 11/15] Clean up types --- src/scripts/app.test.ts | 99 +++++++-------------------------- src/utils/litegraphUtil.test.ts | 72 +++++++----------------- 2 files changed, 42 insertions(+), 129 deletions(-) diff --git a/src/scripts/app.test.ts b/src/scripts/app.test.ts index b5e395ea55e..b77b909191d 100644 --- a/src/scripts/app.test.ts +++ b/src/scripts/app.test.ts @@ -8,6 +8,8 @@ import type { import { ComfyApp } from './app' import { createNode } from '@/utils/litegraphUtil' import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste' +import { useToastStore } from '@/platform/updates/common/toastStore' +import { getWorkflowDataFromFile } from '@/scripts/metadata/parser' vi.mock('@/utils/litegraphUtil', () => ({ createNode: vi.fn(), @@ -35,7 +37,7 @@ vi.mock('@/platform/updates/common/toastStore', () => ({ })) })) -function createMockNode(options: Record = {}): LGraphNode { +function createMockNode(options: { [K in keyof LGraphNode]?: any } = {}) { return { id: 1, pos: [0, 0], @@ -44,7 +46,7 @@ function createMockNode(options: Record = {}): LGraphNode { connect: vi.fn(), getBounding: vi.fn(() => new Float64Array([0, 0, 200, 100])), ...options - } as unknown as LGraphNode + } as LGraphNode } function createMockCanvas(): Partial { @@ -58,21 +60,18 @@ function createMockCanvas(): Partial { } } -function createImageFile( - name: string = 'test.png', - type: string = 'image/png' -): File { +function createTestFile(name: string, type: string): File { return new File([''], name, { type }) } describe('ComfyApp', () => { let app: ComfyApp - let mockCanvas: Partial + let mockCanvas: LGraphCanvas beforeEach(() => { vi.clearAllMocks() app = new ComfyApp() - mockCanvas = createMockCanvas() + mockCanvas = createMockCanvas() as LGraphCanvas app.canvas = mockCanvas as LGraphCanvas }) @@ -85,18 +84,17 @@ describe('ComfyApp', () => { vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1, mockNode2]) vi.mocked(createNode).mockResolvedValue(mockBatchNode) - const file1 = createImageFile('test1.png') - const file2 = createImageFile('test2.jpg', 'image/jpeg') + 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) - await app.handleFileList(dataTransfer.files) + const { files } = dataTransfer - expect(pasteImageNodes).toHaveBeenCalledWith( - mockCanvas, - dataTransfer.files - ) + await app.handleFileList(files) + + expect(pasteImageNodes).toHaveBeenCalledWith(mockCanvas, files) expect(createNode).toHaveBeenCalledWith(mockCanvas, 'BatchImagesNode') expect(mockCanvas.selectItems).toHaveBeenCalledWith([ mockNode1, @@ -112,7 +110,7 @@ describe('ComfyApp', () => { vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1]) vi.mocked(createNode).mockResolvedValue(null) - const file = createImageFile() + const file = createTestFile('test.png', 'image/png') const dataTransfer = new DataTransfer() dataTransfer.items.add(file) @@ -124,15 +122,13 @@ describe('ComfyApp', () => { it('should handle empty file list', async () => { const dataTransfer = new DataTransfer() - - // The implementation doesn't check for empty list and will throw await expect(app.handleFileList(dataTransfer.files)).rejects.toThrow() }) - it('should not process non-image files', async () => { - const textFile = new File([''], 'test.txt', { type: 'text/plain' }) + it('should not process unsupported file types', async () => { + const invalidFile = createTestFile('test.pdf', 'application/pdf') const dataTransfer = new DataTransfer() - dataTransfer.items.add(textFile) + dataTransfer.items.add(invalidFile) await app.handleFileList(dataTransfer.files) @@ -160,71 +156,37 @@ describe('ComfyApp', () => { type: 'LoadImage', getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) }) - const mockNode2 = createMockNode({ - pos: [0, 0], - type: 'LoadImage' - }) - const mockNode3 = createMockNode({ - pos: [0, 0], - type: 'LoadImage' - }) + const mockNode2 = createMockNode({ pos: [0, 0], type: 'LoadImage' }) + const mockNode3 = createMockNode({ pos: [0, 0], type: 'LoadImage' }) const mockBatchNode = createMockNode({ pos: [0, 0] }) app.positionBatchNodes([mockNode1, mockNode2, mockNode3], mockBatchNode) - // Formula: y + (height * index) + (25 * (index + 1)) - // For LoadImage nodes, height = 344 expect(mockNode1.pos).toEqual([100, 200]) - // index 1: 200 + (344 * 1) + (25 * 2) = 200 + 344 + 50 = 594 expect(mockNode2.pos).toEqual([100, 594]) - // index 2: 200 + (344 * 2) + (25 * 3) = 200 + 688 + 75 = 963 expect(mockNode3.pos).toEqual([100, 963]) }) - it('should use set height of 344 for LoadImage nodes', () => { - const mockNode1 = createMockNode({ - pos: [100, 200], - type: 'LoadImage', - getBounding: vi.fn(() => new Float64Array([100, 200, 300, 100])) - }) - const mockNode2 = createMockNode({ - pos: [0, 0], - type: 'LoadImage' - }) - const mockBatchNode = createMockNode({ pos: [0, 0] }) - - app.positionBatchNodes([mockNode1, mockNode2], mockBatchNode) - - // height = max(344, 100) = 344 - // index 1: 200 + (344 * 1) + (25 * 2) = 200 + 344 + 50 = 594 - expect(mockNode2.pos).toEqual([100, 594]) - }) - it('should call graph change once for all nodes', () => { const mockNode1 = createMockNode({ getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400])) }) - const mockNode2 = createMockNode() - const mockNode3 = createMockNode() const mockBatchNode = createMockNode() - app.positionBatchNodes([mockNode1, mockNode2, mockNode3], mockBatchNode) + app.positionBatchNodes([mockNode1], mockBatchNode) - // graph.change() is called for each node in the forEach expect(mockCanvas.graph?.change).toHaveBeenCalledTimes(1) }) }) describe('handleFile', () => { it('should handle image files by creating LoadImage node', async () => { - const { getWorkflowDataFromFile } = - await import('@/scripts/metadata/parser') vi.mocked(getWorkflowDataFromFile).mockResolvedValue({}) const mockNode = createMockNode() vi.mocked(createNode).mockResolvedValue(mockNode) - const imageFile = createImageFile() + const imageFile = createTestFile('test.png', 'image/png') await app.handleFile(imageFile) @@ -235,24 +197,5 @@ describe('ComfyApp', () => { mockNode ) }) - - it('should show error toast for unsupported files', async () => { - const { getWorkflowDataFromFile } = - await import('@/scripts/metadata/parser') - const { useToastStore } = - await import('@/platform/updates/common/toastStore') - const mockAddAlert = vi.fn() - - vi.mocked(getWorkflowDataFromFile).mockResolvedValue({}) - vi.mocked(useToastStore).mockReturnValue({ - addAlert: mockAddAlert - } as unknown as ReturnType) - - const textFile = new File([''], 'test.txt', { type: 'text/plain' }) - - await app.handleFile(textFile) - - expect(mockAddAlert).toHaveBeenCalled() - }) }) }) diff --git a/src/utils/litegraphUtil.test.ts b/src/utils/litegraphUtil.test.ts index 1de11395c0c..0e1a6b4a287 100644 --- a/src/utils/litegraphUtil.test.ts +++ b/src/utils/litegraphUtil.test.ts @@ -1,10 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { - LGraph, - LGraphCanvas, - LGraphNode -} from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation' import type { IWidget } from '@/lib/litegraph/src/types/widgets' @@ -14,6 +9,7 @@ import { createNode, migrateWidgetsValues } from '@/utils/litegraphUtil' +import { useToastStore } from '@/platform/updates/common/toastStore' vi.mock('@/lib/litegraph/src/litegraph', () => ({ LiteGraph: { @@ -23,38 +19,39 @@ vi.mock('@/lib/litegraph/src/litegraph', () => ({ vi.mock('@/platform/updates/common/toastStore', () => ({ useToastStore: vi.fn(() => ({ - addAlert: vi.fn() + addAlert: vi.fn(), + add: vi.fn(), + remove: vi.fn() })) })) vi.mock('@/i18n', () => ({ - t: vi.fn((key) => key) + t: vi.fn((key: string) => key) })) describe('createNode', () => { - let mockCanvas: Partial - let mockGraph: Partial + let mockCanvas: any + let mockGraph: any beforeEach(() => { vi.clearAllMocks() + mockGraph = { add: vi.fn((node) => node), change: vi.fn() } + mockCanvas = { - graph: mockGraph as LGraph, + graph: mockGraph, graph_mouse: [100, 200] } }) it('should create a node successfully', async () => { - const mockNode = { - pos: [0, 0] - } as Partial + const mockNode = { pos: [0, 0] } + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as any) - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) - - const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') + const result = await createNode(mockCanvas, 'LoadImage') expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') expect(mockNode.pos).toEqual([100, 200]) @@ -64,56 +61,29 @@ describe('createNode', () => { }) it('should return null when name is empty', async () => { - const result = await createNode(mockCanvas as LGraphCanvas, '') + const result = await createNode(mockCanvas, '') expect(LiteGraph.createNode).not.toHaveBeenCalled() expect(result).toBeNull() }) - it('should handle node creation failure and show toast', async () => { - const { useToastStore } = - await import('@/platform/updates/common/toastStore') - const mockAddAlert = vi.fn() - vi.mocked(useToastStore).mockReturnValue({ - addAlert: mockAddAlert - } as unknown as ReturnType) - - vi.mocked(LiteGraph.createNode).mockReturnValue(null) - - const result = await createNode(mockCanvas as LGraphCanvas, 'InvalidNode') - - expect(mockAddAlert).toHaveBeenCalledWith('assetBrowser.failedToCreateNode') - expect(result).toBeNull() - }) - it('should handle graph being null', async () => { - const mockNode = { - pos: [0, 0] - } as Partial - + const mockNode = { pos: [0, 0] } mockCanvas.graph = null + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as any) - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) - - const result = await createNode(mockCanvas as LGraphCanvas, 'LoadImage') + const result = await createNode(mockCanvas, 'LoadImage') expect(mockNode.pos).toEqual([0, 0]) expect(result).toBeNull() }) it('should set position based on canvas graph_mouse', async () => { - const mockCanvasWithDifferentPos: Partial = { - ...mockCanvas, - graph_mouse: [250, 350] - } - - const mockNode = { - pos: [0, 0] - } as Partial - - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) + mockCanvas.graph_mouse = [250, 350] + const mockNode = { pos: [0, 0] } + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as any) - await createNode(mockCanvasWithDifferentPos as LGraphCanvas, 'LoadAudio') + await createNode(mockCanvas, 'LoadAudio') expect(mockNode.pos).toEqual([250, 350]) }) From 2ca98501a8f163918088541f7ab2214394f381c5 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 22:24:28 -0600 Subject: [PATCH 12/15] Remove unused import --- src/utils/litegraphUtil.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/litegraphUtil.test.ts b/src/utils/litegraphUtil.test.ts index 0e1a6b4a287..7faad4984ab 100644 --- a/src/utils/litegraphUtil.test.ts +++ b/src/utils/litegraphUtil.test.ts @@ -9,7 +9,6 @@ import { createNode, migrateWidgetsValues } from '@/utils/litegraphUtil' -import { useToastStore } from '@/platform/updates/common/toastStore' vi.mock('@/lib/litegraph/src/litegraph', () => ({ LiteGraph: { From 4252d58a04a1989418e5d28536ef8a5c7c6a266a Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Mon, 2 Feb 2026 22:25:15 -0600 Subject: [PATCH 13/15] Remove unused import --- src/scripts/app.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scripts/app.test.ts b/src/scripts/app.test.ts index b77b909191d..ea632333319 100644 --- a/src/scripts/app.test.ts +++ b/src/scripts/app.test.ts @@ -8,7 +8,6 @@ import type { import { ComfyApp } from './app' import { createNode } from '@/utils/litegraphUtil' import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste' -import { useToastStore } from '@/platform/updates/common/toastStore' import { getWorkflowDataFromFile } from '@/scripts/metadata/parser' vi.mock('@/utils/litegraphUtil', () => ({ From afc6ec8c9fdf022e27320738ceba5d8f26694d1c Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 11 Feb 2026 15:36:58 -0800 Subject: [PATCH 14/15] Remove unused ts-expect-error --- src/services/litegraphService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 0155ead50b7..6ed0927b5d1 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -880,7 +880,6 @@ export const useLitegraphService = () => { const graph = useWorkflowStore().activeSubgraph ?? app.graph - // @ts-expect-error fixme ts strict error graph.add(node) // @ts-expect-error fixme ts strict error return node From 98fc6c6358f6cc99da927a6b3c8e10baffab07a9 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 11 Feb 2026 17:21:32 -0800 Subject: [PATCH 15/15] Fix lints --- src/utils/litegraphUtil.test.ts | 52 ++++++++++++++++++--------------- src/utils/litegraphUtil.ts | 13 +++++---- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/utils/litegraphUtil.test.ts b/src/utils/litegraphUtil.test.ts index 6c61b76e107..a96028a3cd9 100644 --- a/src/utils/litegraphUtil.test.ts +++ b/src/utils/litegraphUtil.test.ts @@ -1,6 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import type { + LGraph, + LGraphCanvas, + LGraphNode +} from '@/lib/litegraph/src/litegraph' import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation' import type { IWidget } from '@/lib/litegraph/src/types/widgets' import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' @@ -28,38 +33,38 @@ vi.mock('@/i18n', () => ({ t: vi.fn((key: string) => key) })) -describe('createNode', () => { - let mockCanvas: any - let mockGraph: any - - beforeEach(() => { - vi.clearAllMocks() - - mockGraph = { - add: vi.fn((node) => node), - change: vi.fn() - } +function createMockCanvas(overrides: Partial = {}): LGraphCanvas { + const mockGraph = { + add: vi.fn((node) => node), + change: vi.fn() + } satisfies Partial as unknown as LGraph + const mockCanvas: Partial = { + graph_mouse: [100, 200], + graph: mockGraph, + ...overrides + } + return mockCanvas as LGraphCanvas +} - mockCanvas = { - graph: mockGraph, - graph_mouse: [100, 200] - } - }) +describe('createNode', () => { + beforeEach(vi.clearAllMocks) it('should create a node successfully', async () => { const mockNode = { pos: [0, 0] } - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as any) + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) + const mockCanvas = createMockCanvas() const result = await createNode(mockCanvas, 'LoadImage') expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') expect(mockNode.pos).toEqual([100, 200]) - expect(mockGraph.add).toHaveBeenCalledWith(mockNode) - expect(mockGraph.change).toHaveBeenCalled() + expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode) + expect(mockCanvas.graph!.change).toHaveBeenCalled() expect(result).toBe(mockNode) }) it('should return null when name is empty', async () => { + const mockCanvas = createMockCanvas() const result = await createNode(mockCanvas, '') expect(LiteGraph.createNode).not.toHaveBeenCalled() @@ -68,19 +73,18 @@ describe('createNode', () => { it('should handle graph being null', async () => { const mockNode = { pos: [0, 0] } - mockCanvas.graph = null - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as any) + const mockCanvas = createMockCanvas({ graph: null }) + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) const result = await createNode(mockCanvas, 'LoadImage') expect(mockNode.pos).toEqual([0, 0]) expect(result).toBeNull() }) - it('should set position based on canvas graph_mouse', async () => { - mockCanvas.graph_mouse = [250, 350] + const mockCanvas = createMockCanvas({ graph_mouse: [250, 350] }) const mockNode = { pos: [0, 0] } - vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as any) + vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode) await createNode(mockCanvas, 'LoadAudio') diff --git a/src/utils/litegraphUtil.ts b/src/utils/litegraphUtil.ts index ed99311eea6..28e1818917b 100644 --- a/src/utils/litegraphUtil.ts +++ b/src/utils/litegraphUtil.ts @@ -3,12 +3,12 @@ import _ from 'es-toolkit/compat' import type { ColorOption, LGraph, - LGraphCanvas, + LGraphCanvas } from '@/lib/litegraph/src/litegraph' -import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LGraphGroup, LGraphNode, + LiteGraph, Reroute, isColorable } from '@/lib/litegraph/src/litegraph' @@ -45,12 +45,15 @@ export async function createNode( return null } - const { graph, graph_mouse: [ posX, posY ] } = canvas + const { + graph, + graph_mouse: [posX, posY] + } = canvas const newNode = LiteGraph.createNode(name) - await new Promise(r => setTimeout(r, 0)) + await new Promise((r) => setTimeout(r, 0)) if (newNode && graph) { - newNode.pos = [ posX, posY ] + newNode.pos = [posX, posY] const addedNode = graph.add(newNode) ?? null if (addedNode) graph.change()