mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
- When in app mode, workflows can be loaded by dragging and dropping as elsewhere. - Dragging a file which is supported by a selected app input to the center panel will apply drop effects on the specific input - This overrides the loading of workflows - There's not currently an indicator for where the image will go. This is being considered for a followup PR - Outputs can be dragged from the assets panel onto nodes - This fixes behaviour outside of app mode as well - Has some thorny implementation specifics - Non-core nodes may not be able to accept these inputs without an update - Node DragOver filtering has reduced functionality when dragging from the assets pane. Nodes may have the blue border without being able to accept a drag operation. - When dropped onto the canvas, the workflow will load (a fix), but the workflow name will be the url of the image preview - The entire card is used for the drag preview <img width="329" height="380" alt="image" src="https://github.com/user-attachments/assets/2945f9a3-3e77-4e14-a812-4a361976390d" /> - Adds a new scroll-shadows tailwind util as an indicator that more content is available by scrolling. - Since a primary goal was preventing API costs overflowing, I've made the indicator fairly strong. This can be tuned later if needed  - Initial support for text outputs in App Mode - Also causes jobs with text outputs to incorrectly display in the assets panel with a generic 'check' icon instead of a text specific icon. This will need a dedicated pass, but shouldn't be overly onerous in the interim. <img width="1209" height="735" alt="text output" src="https://github.com/user-attachments/assets/fcd1cf9f-5d5c-434c-acd0-58d248237b99" /> NOTE: Displaying text outputs conflicted with the changes in #9622. I'll leave text output still disabled in this PR and open a new PR for reconciling text as an output so it can go through dedicated review. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10122-App-Mode-dragAndDrop-text-output-and-scroll-shadows-3256d73d3650810caaf8d75de94388c9) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { flattenNodeOutput } from '@/renderer/extensions/linearMode/flattenNodeOutput'
|
|
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
|
|
|
|
function makeOutput(
|
|
overrides: Partial<NodeExecutionOutput> = {}
|
|
): NodeExecutionOutput {
|
|
return { ...overrides }
|
|
}
|
|
|
|
describe(flattenNodeOutput, () => {
|
|
it('returns empty array for output with no known media types', () => {
|
|
const result = flattenNodeOutput(['1', makeOutput({ unknown: 'hello' })])
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('flattens images into ResultItemImpl instances', () => {
|
|
const output = makeOutput({
|
|
images: [
|
|
{ filename: 'a.png', subfolder: '', type: 'output' },
|
|
{ filename: 'b.png', subfolder: 'sub', type: 'output' }
|
|
]
|
|
})
|
|
|
|
const result = flattenNodeOutput(['42', output])
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].filename).toBe('a.png')
|
|
expect(result[0].nodeId).toBe('42')
|
|
expect(result[0].mediaType).toBe('images')
|
|
expect(result[1].filename).toBe('b.png')
|
|
expect(result[1].subfolder).toBe('sub')
|
|
})
|
|
|
|
it('flattens audio outputs', () => {
|
|
const output = makeOutput({
|
|
audio: [{ filename: 'sound.wav', subfolder: '', type: 'output' }]
|
|
})
|
|
|
|
const result = flattenNodeOutput([7, output])
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].mediaType).toBe('audio')
|
|
expect(result[0].nodeId).toBe(7)
|
|
})
|
|
|
|
it('flattens multiple media types in a single output', () => {
|
|
const output = makeOutput({
|
|
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
|
video: [{ filename: 'vid.mp4', subfolder: '', type: 'output' }]
|
|
})
|
|
|
|
const result = flattenNodeOutput(['1', output])
|
|
|
|
expect(result).toHaveLength(2)
|
|
const types = result.map((r) => r.mediaType)
|
|
expect(types).toContain('images')
|
|
expect(types).toContain('video')
|
|
})
|
|
|
|
it('handles gifs and 3d output types', () => {
|
|
const output = makeOutput({
|
|
gifs: [
|
|
{ filename: 'anim.gif', subfolder: '', type: 'output' }
|
|
] as NodeExecutionOutput['gifs'],
|
|
'3d': [
|
|
{ filename: 'model.glb', subfolder: '', type: 'output' }
|
|
] as NodeExecutionOutput['3d']
|
|
})
|
|
|
|
const result = flattenNodeOutput(['5', output])
|
|
|
|
expect(result).toHaveLength(2)
|
|
const types = result.map((r) => r.mediaType)
|
|
expect(types).toContain('gifs')
|
|
expect(types).toContain('3d')
|
|
})
|
|
|
|
it('ignores empty arrays', () => {
|
|
const output = makeOutput({ images: [], audio: [] })
|
|
const result = flattenNodeOutput(['1', output])
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('flattens non-standard output keys with ResultItem-like values', () => {
|
|
const output = makeOutput({
|
|
a_images: [{ filename: 'before.png', subfolder: '', type: 'output' }],
|
|
b_images: [{ filename: 'after.png', subfolder: '', type: 'output' }]
|
|
} as unknown as Partial<NodeExecutionOutput>)
|
|
|
|
const result = flattenNodeOutput(['10', output])
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result.map((r) => r.filename)).toContain('before.png')
|
|
expect(result.map((r) => r.filename)).toContain('after.png')
|
|
})
|
|
|
|
it('excludes animated key', () => {
|
|
const output = makeOutput({
|
|
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
|
animated: [true]
|
|
})
|
|
|
|
const result = flattenNodeOutput(['1', output])
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].mediaType).toBe('images')
|
|
})
|
|
|
|
it('excludes non-ResultItem array items', () => {
|
|
const output = {
|
|
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
|
custom_data: [{ randomKey: 123 }]
|
|
} as unknown as NodeExecutionOutput
|
|
|
|
const result = flattenNodeOutput(['1', output])
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].mediaType).toBe('images')
|
|
})
|
|
|
|
it('accepts items with filename but no subfolder', () => {
|
|
const output = {
|
|
images: [
|
|
{ filename: 'valid.png', subfolder: '', type: 'output' },
|
|
{ filename: 'no-subfolder.png' }
|
|
]
|
|
} as unknown as NodeExecutionOutput
|
|
|
|
const result = flattenNodeOutput(['1', output])
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].filename).toBe('valid.png')
|
|
expect(result[1].filename).toBe('no-subfolder.png')
|
|
expect(result[1].subfolder).toBe('')
|
|
})
|
|
|
|
it('excludes items missing filename', () => {
|
|
const output = {
|
|
images: [
|
|
{ filename: 'valid.png', subfolder: '', type: 'output' },
|
|
{ subfolder: '', type: 'output' }
|
|
]
|
|
} as unknown as NodeExecutionOutput
|
|
|
|
const result = flattenNodeOutput(['1', output])
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].filename).toBe('valid.png')
|
|
})
|
|
})
|