mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-14 19:51:05 +00:00
Compare commits
1 Commits
fix/codera
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cb09b823d |
@@ -2,13 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const { mockFetchApi, mockAddAlert, mockUpdateInputs, mockSetNodeUploading } =
|
||||
vi.hoisted(() => ({
|
||||
mockFetchApi: vi.fn(),
|
||||
mockAddAlert: vi.fn(),
|
||||
mockUpdateInputs: vi.fn(),
|
||||
mockSetNodeUploading: vi.fn()
|
||||
}))
|
||||
const { mockFetchApi, mockAddAlert, mockUpdateInputs } = vi.hoisted(() => ({
|
||||
mockFetchApi: vi.fn(),
|
||||
mockAddAlert: vi.fn(),
|
||||
mockUpdateInputs: vi.fn()
|
||||
}))
|
||||
|
||||
let capturedDragOnDrop: (files: File[]) => Promise<string[]>
|
||||
|
||||
@@ -45,10 +43,6 @@ vi.mock('@/stores/assetsStore', () => ({
|
||||
useAssetsStore: () => ({ updateInputs: mockUpdateInputs })
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/nodeOutputStore', () => ({
|
||||
useNodeOutputStore: () => ({ setNodeUploading: mockSetNodeUploading })
|
||||
}))
|
||||
|
||||
function createMockNode(): LGraphNode {
|
||||
return {
|
||||
isUploading: false,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
|
||||
const PASTED_IMAGE_EXPIRY_MS = 2000
|
||||
|
||||
@@ -93,15 +92,12 @@ export const useNodeImageUpload = (
|
||||
}
|
||||
}
|
||||
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const handleUploadBatch = async (files: File[]) => {
|
||||
if (node.isUploading) {
|
||||
useToastStore().addAlert(t('g.uploadAlreadyInProgress'))
|
||||
return []
|
||||
}
|
||||
node.isUploading = true
|
||||
nodeOutputStore.setNodeUploading(node.id, true)
|
||||
|
||||
try {
|
||||
node.imgs = undefined
|
||||
@@ -118,7 +114,6 @@ export const useNodeImageUpload = (
|
||||
return validPaths
|
||||
} finally {
|
||||
node.isUploading = false
|
||||
nodeOutputStore.setNodeUploading(node.id, false)
|
||||
node.graph?.setDirtyCanvas(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,6 @@
|
||||
"control_before_generate": "control before generate",
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"uploadAlreadyInProgress": "Upload already in progress",
|
||||
"uploading": "Uploading",
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"nodesCount": "{count} node | {count} nodes",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<!-- Main Image -->
|
||||
<img
|
||||
v-if="!imageError && !isUploading"
|
||||
v-if="!imageError"
|
||||
:src="currentImageUrl"
|
||||
:alt="imageAltText"
|
||||
class="pointer-events-none absolute inset-0 block size-full object-contain"
|
||||
@@ -45,16 +45,6 @@
|
||||
@error="handleImageError"
|
||||
/>
|
||||
|
||||
<!-- Upload Spinner Overlay -->
|
||||
<div
|
||||
v-if="isUploading"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--loader-circle] size-8 animate-spin text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Buttons (appear on hover and focus) -->
|
||||
<div
|
||||
v-if="isHovered || isFocused"
|
||||
@@ -95,10 +85,7 @@
|
||||
|
||||
<!-- Image Dimensions -->
|
||||
<div class="pt-2 text-center text-xs text-base-foreground">
|
||||
<span v-if="isUploading" class="text-base-foreground">
|
||||
{{ $t('g.uploading') }}...
|
||||
</span>
|
||||
<span v-else-if="imageError" class="text-red-400">
|
||||
<span v-if="imageError" class="text-red-400">
|
||||
{{ $t('g.errorLoadingImage') }}
|
||||
</span>
|
||||
<span v-else-if="showLoader" class="text-base-foreground">
|
||||
@@ -147,8 +134,6 @@ interface ImagePreviewProps {
|
||||
readonly imageUrls: readonly string[]
|
||||
/** Optional node ID for context-aware actions */
|
||||
readonly nodeId?: string
|
||||
/** Whether a file is being uploaded to this node */
|
||||
readonly isUploading?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<ImagePreviewProps>()
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
v-if="nodeMedia"
|
||||
:node-data="nodeData"
|
||||
:media="nodeMedia"
|
||||
:is-uploading="isNodeUploading"
|
||||
/>
|
||||
<NodeContent
|
||||
v-for="preview in promotedPreviews"
|
||||
@@ -745,8 +744,6 @@ const nodeMedia = computed(() => {
|
||||
return { type, urls } as const
|
||||
})
|
||||
|
||||
const isNodeUploading = computed(() => nodeOutputs.isNodeUploading(nodeData.id))
|
||||
|
||||
const nodeContainerRef = ref<HTMLDivElement>()
|
||||
|
||||
// Drag and drop support
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
v-else-if="hasMedia && media?.type === 'image'"
|
||||
:image-urls="media.urls"
|
||||
:node-id="nodeId"
|
||||
:is-uploading="isUploading"
|
||||
class="mt-2 flex-auto"
|
||||
/>
|
||||
</slot>
|
||||
@@ -44,15 +43,14 @@ interface NodeContentProps {
|
||||
type: 'image' | 'video' | 'audio'
|
||||
urls: string[]
|
||||
}
|
||||
isUploading?: boolean
|
||||
}
|
||||
|
||||
const { nodeData, media, isUploading = false } = defineProps<NodeContentProps>()
|
||||
const props = defineProps<NodeContentProps>()
|
||||
|
||||
const hasMedia = computed(() => media && media.urls.length > 0)
|
||||
const hasMedia = computed(() => props.media && props.media.urls.length > 0)
|
||||
|
||||
// Get node ID from nodeData
|
||||
const nodeId = computed(() => nodeData?.id?.toString())
|
||||
const nodeId = computed(() => props.nodeData?.id?.toString())
|
||||
|
||||
// Error boundary implementation
|
||||
const renderError = ref<string | null>(null)
|
||||
|
||||
@@ -7,18 +7,11 @@ import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import * as litegraphUtil from '@/utils/litegraphUtil'
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isAnimatedOutput: vi.fn(),
|
||||
isVideoNode: vi.fn()
|
||||
}))
|
||||
|
||||
const mockGetNodeById = vi.fn()
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
getPreviewFormatParam: vi.fn(() => '&format=test_webp'),
|
||||
rootGraph: {
|
||||
getNodeById: (...args: unknown[]) => mockGetNodeById(...args)
|
||||
},
|
||||
@@ -151,76 +144,6 @@ describe('nodeOutputStore restoreOutputs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeOutputStore getPreviewParam', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(litegraphUtil.isAnimatedOutput).mockReturnValue(false)
|
||||
vi.mocked(litegraphUtil.isVideoNode).mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('should return empty string if output is animated', () => {
|
||||
const store = useNodeOutputStore()
|
||||
vi.mocked(litegraphUtil.isAnimatedOutput).mockReturnValue(true)
|
||||
const node = createMockNode()
|
||||
const outputs = createMockOutputs([{ filename: 'img.png' }])
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return empty string if isVideoNode returns true', () => {
|
||||
const store = useNodeOutputStore()
|
||||
vi.mocked(litegraphUtil.isVideoNode).mockReturnValue(true)
|
||||
const node = createMockNode()
|
||||
const outputs = createMockOutputs([{ filename: 'img.png' }])
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return empty string if outputs.images is undefined', () => {
|
||||
const store = useNodeOutputStore()
|
||||
const node = createMockNode()
|
||||
const outputs: ExecutedWsMessage['output'] = {}
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return empty string if outputs.images is empty', () => {
|
||||
const store = useNodeOutputStore()
|
||||
const node = createMockNode()
|
||||
const outputs = createMockOutputs([])
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return empty string if outputs.images contains SVG images', () => {
|
||||
const store = useNodeOutputStore()
|
||||
const node = createMockNode()
|
||||
const outputs = createMockOutputs([{ filename: 'img.svg' }])
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return format param for standard image outputs', () => {
|
||||
const store = useNodeOutputStore()
|
||||
const node = createMockNode()
|
||||
const outputs = createMockOutputs([{ filename: 'img.png' }])
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('&format=test_webp')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return format param for multiple standard images', () => {
|
||||
const store = useNodeOutputStore()
|
||||
const node = createMockNode()
|
||||
const outputs = createMockOutputs([
|
||||
{ filename: 'img1.png' },
|
||||
{ filename: 'img2.jpg' }
|
||||
])
|
||||
expect(store.getPreviewParam(node, outputs)).toBe('&format=test_webp')
|
||||
expect(vi.mocked(app).getPreviewFormatParam).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
ResultItemType
|
||||
} from '@/schemas/apiSchema'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { clone } from '@/scripts/utils'
|
||||
@@ -48,22 +50,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const scheduledRevoke: Record<NodeLocatorId, { stop: () => void }> = {}
|
||||
const latestPreview = ref<string[]>([])
|
||||
|
||||
const uploadingNodeIds = ref(new Set<string>())
|
||||
|
||||
function setNodeUploading(nodeId: string | number, uploading: boolean) {
|
||||
const id = String(nodeId)
|
||||
if (uploading) {
|
||||
uploadingNodeIds.value.add(id)
|
||||
} else {
|
||||
uploadingNodeIds.value.delete(id)
|
||||
}
|
||||
uploadingNodeIds.value = new Set(uploadingNodeIds.value)
|
||||
}
|
||||
|
||||
function isNodeUploading(nodeId: string | number): boolean {
|
||||
return uploadingNodeIds.value.has(String(nodeId))
|
||||
}
|
||||
|
||||
function scheduleRevoke(locator: NodeLocatorId, cb: () => void) {
|
||||
scheduledRevoke[locator]?.stop()
|
||||
|
||||
@@ -113,20 +99,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preview param for the node's outputs.
|
||||
*
|
||||
* If the output is an image, use the user's preferred format (from settings).
|
||||
* For non-image outputs, return an empty string, as including the preview param
|
||||
* will force the server to load the output file as an image.
|
||||
*/
|
||||
function getPreviewParam(
|
||||
node: LGraphNode,
|
||||
outputs: ExecutedWsMessage['output']
|
||||
): string {
|
||||
return isImageOutputs(node, outputs) ? app.getPreviewFormatParam() : ''
|
||||
}
|
||||
|
||||
function getNodeImageUrls(node: LGraphNode): string[] | undefined {
|
||||
const previews = getNodePreviews(node)
|
||||
if (previews?.length) return previews
|
||||
@@ -134,14 +106,17 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const outputs = getNodeOutputs(node)
|
||||
if (!outputs?.images?.length) return
|
||||
|
||||
const rand = app.getRandParam()
|
||||
const previewParam = getPreviewParam(node, outputs)
|
||||
const isImage = isImageOutputs(node, outputs)
|
||||
|
||||
return outputs.images.map((image) => {
|
||||
const params = new URLSearchParams(image)
|
||||
if (isImage) appendCloudResParam(params, image.filename)
|
||||
return api.apiURL(`/view?${params}${previewParam}${rand}`)
|
||||
if (isImage) {
|
||||
appendCloudResParam(params, image.filename)
|
||||
const previewFormat = useSettingStore().get('Comfy.PreviewFormat')
|
||||
if (previewFormat) params.set('preview', previewFormat)
|
||||
}
|
||||
if (!isCloud) params.set('rand', String(Math.random()))
|
||||
return api.apiURL(`/view?${params}`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -459,8 +434,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
getNodeOutputs,
|
||||
getNodeImageUrls,
|
||||
getNodePreviews,
|
||||
getPreviewParam,
|
||||
|
||||
// Setters
|
||||
setNodeOutputs,
|
||||
setNodeOutputsByExecutionId,
|
||||
@@ -479,10 +452,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
restoreOutputs,
|
||||
resetAllOutputsAndPreviews,
|
||||
|
||||
// Upload state
|
||||
setNodeUploading,
|
||||
isNodeUploading,
|
||||
|
||||
// State
|
||||
nodeOutputs,
|
||||
nodePreviewImages,
|
||||
|
||||
Reference in New Issue
Block a user