mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 11:11:00 +00:00
Compare commits
1 Commits
codex/clou
...
codex/qpo-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bdcf21057 |
100
src/components/sidebar/tabs/AssetsSidebarGridView.test.ts
Normal file
100
src/components/sidebar/tabs/AssetsSidebarGridView.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
|
||||
const { jobItems, settingGetMock } = vi.hoisted(() => ({
|
||||
jobItems: [] as JobListItem[],
|
||||
settingGetMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useJobList', () => ({
|
||||
useJobList: () => ({
|
||||
jobItems: {
|
||||
get value() {
|
||||
return jobItems
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: settingGetMock
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/components/MediaAssetCard.vue', () => ({
|
||||
default: {
|
||||
name: 'MediaAssetCard',
|
||||
template: '<div class="media-asset-card-stub" />'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/components/ActiveMediaAssetCard.vue', () => ({
|
||||
default: {
|
||||
name: 'ActiveMediaAssetCard',
|
||||
emits: ['context-menu'],
|
||||
template:
|
||||
'<div class="active-media-asset-card-stub" @contextmenu="$emit(\'context-menu\', $event)" />'
|
||||
}
|
||||
}))
|
||||
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: {} }
|
||||
})
|
||||
|
||||
const VirtualGridStub = {
|
||||
name: 'VirtualGrid',
|
||||
props: {
|
||||
items: { type: Array, required: true }
|
||||
},
|
||||
template:
|
||||
'<div><div v-for="item in items" :key="item.key"><slot name="item" :item="item" /></div></div>'
|
||||
}
|
||||
|
||||
const createJobItem = (overrides: Partial<JobListItem> = {}): JobListItem => ({
|
||||
id: 'job-1',
|
||||
title: 'Active job',
|
||||
meta: 'In progress',
|
||||
state: 'running',
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('AssetsSidebarGridView', () => {
|
||||
beforeEach(() => {
|
||||
jobItems.splice(0, jobItems.length)
|
||||
settingGetMock.mockReset()
|
||||
settingGetMock.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('emits job context menu for active job cards in QPOV2', async () => {
|
||||
const activeJob = createJobItem()
|
||||
jobItems.push(activeJob)
|
||||
|
||||
const wrapper = mount(AssetsSidebarGridView, {
|
||||
props: {
|
||||
assets: [],
|
||||
isSelected: () => false,
|
||||
showOutputCount: () => false,
|
||||
getOutputCount: () => 0
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
VirtualGrid: VirtualGridStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.get('.active-media-asset-card-stub').trigger('contextmenu')
|
||||
|
||||
expect(wrapper.emitted('job-context-menu')).toHaveLength(1)
|
||||
expect(wrapper.emitted('job-context-menu')?.[0]?.[1]).toEqual(activeJob)
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,7 @@
|
||||
v-for="job in activeJobItems"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@context-menu="emit('job-context-menu', $event, job)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +61,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import ActiveMediaAssetCard from '@/platform/assets/components/ActiveMediaAssetCard.vue'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
@@ -84,6 +86,7 @@ const {
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-asset', asset: AssetItem): void
|
||||
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
|
||||
(e: 'job-context-menu', event: MouseEvent, job: JobListItem): void
|
||||
(e: 'approach-end'): void
|
||||
(e: 'zoom', asset: AssetItem): void
|
||||
(e: 'output-count-click', asset: AssetItem): void
|
||||
|
||||
113
src/components/sidebar/tabs/AssetsSidebarListView.test.ts
Normal file
113
src/components/sidebar/tabs/AssetsSidebarListView.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { computed } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
|
||||
const { jobItems, settingGetMock } = vi.hoisted(() => ({
|
||||
jobItems: [] as JobListItem[],
|
||||
settingGetMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useJobList', () => ({
|
||||
useJobList: () => ({
|
||||
jobItems: {
|
||||
get value() {
|
||||
return jobItems
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: settingGetMock
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/assetsStore', () => ({
|
||||
useAssetsStore: () => ({
|
||||
isAssetDeleting: () => false
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useJobActions', () => ({
|
||||
useJobActions: () => ({
|
||||
cancelAction: {
|
||||
icon: 'icon-[lucide--x]',
|
||||
label: 'Cancel',
|
||||
variant: 'destructive'
|
||||
},
|
||||
canCancelJob: computed(() => true),
|
||||
runCancelJob: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: {} }
|
||||
})
|
||||
|
||||
const VirtualGridStub = {
|
||||
name: 'VirtualGrid',
|
||||
props: {
|
||||
items: { type: Array, required: true }
|
||||
},
|
||||
template:
|
||||
'<div><div v-for="item in items" :key="item.key"><slot name="item" :item="item" /></div></div>'
|
||||
}
|
||||
|
||||
const AssetsListItemStub = {
|
||||
name: 'AssetsListItem',
|
||||
emits: ['contextmenu'],
|
||||
template:
|
||||
'<div class="assets-list-item-stub" @contextmenu="$emit(\'contextmenu\', $event)"><slot name="actions" /></div>'
|
||||
}
|
||||
|
||||
const createJobItem = (overrides: Partial<JobListItem> = {}): JobListItem => ({
|
||||
id: 'job-1',
|
||||
title: 'Active job',
|
||||
meta: 'In progress',
|
||||
state: 'running',
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('AssetsSidebarListView', () => {
|
||||
beforeEach(() => {
|
||||
jobItems.splice(0, jobItems.length)
|
||||
settingGetMock.mockReset()
|
||||
settingGetMock.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('emits job context menu for active jobs in QPOV2 list mode', async () => {
|
||||
const activeJob = createJobItem()
|
||||
jobItems.push(activeJob)
|
||||
|
||||
const wrapper = mount(AssetsSidebarListView, {
|
||||
props: {
|
||||
assets: [],
|
||||
isSelected: () => false
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
VirtualGrid: VirtualGridStub,
|
||||
AssetsListItem: AssetsListItemStub,
|
||||
Button: true,
|
||||
LoadingOverlay: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wrapper
|
||||
.findComponent(AssetsListItemStub)
|
||||
.vm.$emit('contextmenu', new MouseEvent('contextmenu'))
|
||||
|
||||
expect(wrapper.emitted('job-context-menu')).toHaveLength(1)
|
||||
expect(wrapper.emitted('job-context-menu')?.[0]?.[1]).toEqual(activeJob)
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,7 @@
|
||||
:progress-current-percent="job.progressCurrentPercent"
|
||||
@mouseenter="onJobEnter(job.id)"
|
||||
@mouseleave="onJobLeave(job.id)"
|
||||
@contextmenu.prevent.stop="emit('job-context-menu', $event, job)"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="hoveredJobId === job.id" #actions>
|
||||
@@ -150,6 +151,7 @@ const assetsStore = useAssetsStore()
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-asset', asset: AssetItem): void
|
||||
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
|
||||
(e: 'job-context-menu', event: MouseEvent, job: JobListItem): void
|
||||
(e: 'approach-end'): void
|
||||
}>()
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
:asset-type="activeTab"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@job-context-menu="handleJobContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
/>
|
||||
<AssetsSidebarGridView
|
||||
@@ -114,6 +115,7 @@
|
||||
:get-output-count="getOutputCount"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@job-context-menu="handleJobContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
@zoom="handleZoomClick"
|
||||
@output-count-click="enterFolderView"
|
||||
@@ -198,6 +200,11 @@
|
||||
@bulk-open-workflow="handleBulkOpenWorkflow"
|
||||
@bulk-export-workflow="handleBulkExportWorkflow"
|
||||
/>
|
||||
<JobContextMenu
|
||||
ref="jobContextMenuRef"
|
||||
:entries="jobMenuEntries"
|
||||
@action="onJobMenuAction"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -218,6 +225,7 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
import JobContextMenu from '@/components/queue/job/JobContextMenu.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
@@ -225,7 +233,13 @@ import TabList from '@/components/tab/TabList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import MediaAssetContextMenu from '@/platform/assets/components/MediaAssetContextMenu.vue'
|
||||
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
|
||||
import { getAssetType } from '@/platform/assets/composables/media/assetMappers'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import type { MenuEntry } from '@/composables/queue/useJobMenu'
|
||||
import { useJobMenu } from '@/composables/queue/useJobMenu'
|
||||
import {
|
||||
getAssetType,
|
||||
mapTaskOutputToAssetItem
|
||||
} from '@/platform/assets/composables/media/assetMappers'
|
||||
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
||||
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
|
||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||
@@ -273,6 +287,20 @@ const isListView = computed(
|
||||
|
||||
const contextMenuRef = ref<InstanceType<typeof MediaAssetContextMenu>>()
|
||||
const contextMenuAsset = ref<AssetItem | null>(null)
|
||||
const currentJobMenuItem = ref<JobListItem | null>(null)
|
||||
const jobContextMenuRef = ref<InstanceType<typeof JobContextMenu> | null>(null)
|
||||
|
||||
const onInspectJobAsset = (item: JobListItem) => {
|
||||
const task = item.taskRef
|
||||
const preview = task?.previewOutput
|
||||
if (!task || !preview) return
|
||||
handleZoomClick(mapTaskOutputToAssetItem(task, preview))
|
||||
}
|
||||
|
||||
const { jobMenuEntries } = useJobMenu(
|
||||
() => currentJobMenuItem.value,
|
||||
onInspectJobAsset
|
||||
)
|
||||
|
||||
// Determine if delete button should be shown
|
||||
// Hide delete button when in input tab and not in cloud (OSS mode - files are from local folders)
|
||||
@@ -482,6 +510,17 @@ function handleAssetContextMenu(event: MouseEvent, asset: AssetItem) {
|
||||
})
|
||||
}
|
||||
|
||||
function handleJobContextMenu(event: MouseEvent, job: JobListItem) {
|
||||
currentJobMenuItem.value = job
|
||||
jobContextMenuRef.value?.open(event)
|
||||
}
|
||||
|
||||
const onJobMenuAction = async (entry: MenuEntry) => {
|
||||
if (entry.kind === 'divider') return
|
||||
if (entry.onClick) await entry.onClick()
|
||||
jobContextMenuRef.value?.hide()
|
||||
}
|
||||
|
||||
function handleContextMenuHide() {
|
||||
// Delay clearing to allow command callbacks to emit before component unmounts
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
tabindex="0"
|
||||
:aria-label="t('sideToolbar.activeJobStatus', { status: statusText })"
|
||||
class="flex flex-col gap-2 p-2 rounded-lg"
|
||||
@contextmenu.stop.prevent="emit('context-menu', $event)"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
@focusin="hovered = true"
|
||||
@@ -86,6 +87,9 @@ import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||
|
||||
const { job } = defineProps<{ job: JobListItem }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'context-menu', event: MouseEvent): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const hovered = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user