Compare commits

...

20 Commits

Author SHA1 Message Date
Benjamin Lu
003e72406d refactor: move queue files under domain 2026-01-08 20:00:49 -08:00
Benjamin Lu
5561efcfd3 Add ... context menu to list view 2025-12-31 18:23:22 -08:00
Benjamin Lu
79af71530d fix: avoid unused export in job actions 2025-12-31 18:17:59 -08:00
Benjamin Lu
f78b6eec47 Add and use knipIgnoreUnusedButUsedByStorybook 2025-12-31 18:16:07 -08:00
Benjamin Lu
d8e57c60bf Add stories for list view and general job card 2025-12-31 18:16:07 -08:00
Benjamin Lu
db3edd522d Add list view 2025-12-31 18:14:48 -08:00
Benjamin Lu
f128c61c53 Remove special failed job styling 2025-12-31 18:10:17 -08:00
GitHub Action
afa4664ad5 [automated] Apply ESLint and Prettier fixes 2025-12-31 17:57:26 -08:00
Benjamin Lu
627db6784e knip 2025-12-31 17:56:08 -08:00
Benjamin Lu
85c6825a79 Add list view 2025-12-31 17:56:08 -08:00
Benjamin Lu
f614914fdf Remove view action from AssetsListCard story 2025-12-31 17:52:10 -08:00
Benjamin Lu
da4889900e Add AssetsListCard stories 2025-12-31 17:52:10 -08:00
Benjamin Lu
5b1456896b Add AssetsListCard base template 2025-12-31 17:52:10 -08:00
Benjamin Lu
0c6ea56360 fix: read QPOV2 setting in assets sidebar 2025-12-31 16:54:17 -08:00
Benjamin Lu
2fd9a73b68 Readd divider as v-else 2025-12-31 15:05:30 -08:00
Benjamin Lu
dc53cbe3c9 Add N active jobs and clear queue button 2025-12-31 15:05:30 -08:00
Benjamin Lu
3d0c0d16ca Move feature flag to setting 2025-12-31 14:59:15 -08:00
Benjamin Lu
a0dad31e2f Add feature flag 2025-12-31 14:59:15 -08:00
Benjamin Lu
5ddea4e7b6 Extract to component 2025-12-31 14:59:15 -08:00
Benjamin Lu
94884d7a7c feat: add queue view toggle stub 2025-12-31 14:59:15 -08:00
80 changed files with 1126 additions and 165 deletions

View File

@@ -69,9 +69,32 @@ const config: StorybookConfig = {
allowedHosts: true
},
resolve: {
alias: {
'@': process.cwd() + '/src'
}
alias: [
{
find: '@/composables/queue/useJobList',
replacement: process.cwd() + '/src/storybook/mocks/useJobList.ts'
},
{
find: '@/composables/queue/useJobActions',
replacement: process.cwd() + '/src/storybook/mocks/useJobActions.ts'
},
{
find: '@/utils/formatUtil',
replacement:
process.cwd() +
'/packages/shared-frontend-utils/src/formatUtil.ts'
},
{
find: '@/utils/networkUtil',
replacement:
process.cwd() +
'/packages/shared-frontend-utils/src/networkUtil.ts'
},
{
find: '@',
replacement: process.cwd() + '/src'
}
]
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main

View File

@@ -1,6 +1,6 @@
import type { Locator, Page } from '@playwright/test'
import type { AutoQueueMode } from '../../src/stores/queueStore'
import type { AutoQueueMode } from '../../src/queue/stores/queueStore'
export class ComfyActionbar {
public readonly root: Locator

View File

@@ -66,7 +66,8 @@ const config: KnipConfig = {
},
tags: [
'-knipIgnoreUnusedButUsedByCustomNodes',
'-knipIgnoreUnusedButUsedByVueNodesBranch'
'-knipIgnoreUnusedButUsedByVueNodesBranch',
'-knipIgnoreUnusedButUsedByStorybook'
]
}

View File

@@ -83,7 +83,7 @@ import { useI18n } from 'vue-i18n'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import QueueProgressOverlay from '@/queue/components/QueueProgressOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
@@ -92,7 +92,7 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { app } from '@/scripts/app'
import { useQueueStore } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'

View File

@@ -38,7 +38,7 @@ import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useQueueSettingsStore } from '@/queue/stores/queueStore'
const queueSettingsStore = useQueueSettingsStore()
const { batchCount } = storeToRefs(queueSettingsStore)

View File

@@ -48,7 +48,7 @@ import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useQueueSettingsStore } from '@/queue/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'

View File

@@ -0,0 +1,155 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { JobAction } from '@/queue/composables/useJobActions'
import type { JobListItem } from '@/queue/composables/useJobList'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { setMockJobActions } from '@/storybook/mocks/useJobActions'
import { setMockJobItems } from '@/storybook/mocks/useJobList'
import { iconForJobState } from '@/queue/utils/queueDisplay'
import AssetsSidebarListView from './AssetsSidebarListView.vue'
type StoryArgs = {
assets: AssetItem[]
jobs: JobListItem[]
selectedAssetIds?: string[]
actionsByJobId?: Record<string, JobAction[]>
}
function baseDecorator() {
return {
template: `
<div class="bg-base-background p-6">
<story />
</div>
`
}
}
const meta: Meta<StoryArgs> = {
title: 'Components/Sidebar/AssetsSidebarListView',
component: AssetsSidebarListView,
parameters: {
layout: 'centered'
},
decorators: [baseDecorator]
}
export default meta
type Story = StoryObj<typeof meta>
const baseTimestamp = '2024-01-15T10:00:00Z'
const sampleJobs: JobListItem[] = [
{
id: 'job-pending-1',
title: 'In queue',
meta: '8:59:30pm',
state: 'pending',
iconName: iconForJobState('pending'),
showClear: true
},
{
id: 'job-init-1',
title: 'Initializing...',
meta: '8:59:35pm',
state: 'initialization',
iconName: iconForJobState('initialization'),
showClear: true
},
{
id: 'job-running-1',
title: 'Total: 30%',
meta: 'KSampler: 70%',
state: 'running',
iconName: iconForJobState('running'),
showClear: true,
progressTotalPercent: 30,
progressCurrentPercent: 70
}
]
const sampleAssets: AssetItem[] = [
{
id: 'asset-image-1',
name: 'image-032.png',
created_at: baseTimestamp,
preview_url: '/assets/images/comfy-logo-single.svg',
size: 1887437,
tags: [],
user_metadata: {
promptId: 'job-running-1',
nodeId: 12,
executionTimeInSeconds: 1.84
}
},
{
id: 'asset-video-1',
name: 'clip-01.mp4',
created_at: baseTimestamp,
preview_url: '/assets/images/default-template.png',
size: 8394820,
tags: [],
user_metadata: {
duration: 132000
}
},
{
id: 'asset-audio-1',
name: 'soundtrack-01.mp3',
created_at: baseTimestamp,
size: 5242880,
tags: [],
user_metadata: {
duration: 200000
}
},
{
id: 'asset-3d-1',
name: 'scene-01.glb',
created_at: baseTimestamp,
size: 134217728,
tags: []
}
]
const cancelAction: JobAction = {
key: 'cancel',
icon: 'icon-[lucide--x]',
label: 'Cancel',
variant: 'destructive'
}
export const RunningAndGenerated: Story = {
args: {
assets: sampleAssets,
jobs: sampleJobs,
actionsByJobId: {
'job-pending-1': [cancelAction],
'job-init-1': [cancelAction],
'job-running-1': [cancelAction]
}
},
render: renderAssetsSidebarListView
}
function renderAssetsSidebarListView(args: StoryArgs) {
return {
components: { AssetsSidebarListView },
setup() {
setMockJobItems(args.jobs)
setMockJobActions(args.actionsByJobId ?? {})
const selectedIds = new Set(args.selectedAssetIds ?? [])
function isSelected(assetId: string) {
return selectedIds.has(assetId)
}
return { args, isSelected }
},
template: `
<div class="h-[520px] w-[320px] overflow-hidden rounded-lg border border-panel-border">
<AssetsSidebarListView :assets="args.assets" :is-selected="isSelected" />
</div>
`
}
}

View File

@@ -0,0 +1,241 @@
<template>
<div class="flex h-full flex-col">
<div v-if="activeJobItems.length" class="flex flex-col gap-2 px-2">
<AssetsListCard
v-for="job in activeJobItems"
:key="job.id"
:class="getJobCardClass()"
:preview-url="job.iconImageUrl"
:preview-alt="job.title"
:icon-name="job.iconName"
:icon-class="getJobIconClass(job)"
:primary-text="job.title"
:secondary-text="job.meta"
:progress-total-percent="job.progressTotalPercent"
:progress-current-percent="job.progressCurrentPercent"
@mouseenter="onJobEnter(job.id)"
@mouseleave="onJobLeave(job.id)"
@click.stop
>
<template
v-if="hoveredJobId === job.id && getJobActions(job).length"
#actions
>
<Button
v-for="action in getJobActions(job)"
:key="action.key"
:variant="action.variant"
size="icon"
:aria-label="action.label"
@click.stop="handleJobAction(action, job)"
>
<i :class="action.icon" class="size-4" />
</Button>
</template>
</AssetsListCard>
</div>
<div
v-if="assets.length"
:class="cn('px-2', activeJobItems.length && 'mt-2')"
>
<div
class="flex items-center py-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
>
{{ t('sideToolbar.generatedAssetsHeader') }}
</div>
</div>
<VirtualGrid
class="flex-1"
:items="assetItems"
:grid-style="listGridStyle"
@approach-end="emit('approach-end')"
>
<template #item="{ item }">
<AssetsListCard
role="button"
tabindex="0"
:aria-label="
t('assetBrowser.ariaLabel.assetCard', {
name: item.asset.name,
type: getMediaTypeFromFilename(item.asset.name)
})
"
:class="getAssetCardClass(isSelected(item.asset.id))"
:preview-url="item.asset.preview_url"
:preview-alt="item.asset.name"
:icon-name="getAssetIconName(item.asset)"
:primary-text="getAssetPrimaryText(item.asset)"
:secondary-text="getAssetSecondaryText(item.asset)"
@mouseenter="onAssetEnter(item.asset.id)"
@mouseleave="onAssetLeave(item.asset.id)"
@contextmenu.prevent="handleAssetMenuClick($event, item.asset)"
@click.stop="emit('select-asset', item.asset)"
>
<template v-if="hoveredAssetId === item.asset.id" #actions>
<Button
variant="secondary"
size="icon"
:aria-label="t('g.moreOptions')"
@click.stop="handleAssetMenuClick($event, item.asset)"
>
<i class="icon-[lucide--ellipsis] size-4" />
</Button>
</template>
</AssetsListCard>
</template>
</VirtualGrid>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Button from '@/components/ui/button/Button.vue'
import type { JobAction } from '@/queue/composables/useJobActions'
import { useJobActions } from '@/queue/composables/useJobActions'
import type { JobListItem } from '@/queue/composables/useJobList'
import { useJobList } from '@/queue/composables/useJobList'
import AssetsListCard from '@/platform/assets/components/AssetsListCard.vue'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { JobState } from '@/queue/types/queue'
import {
formatDuration,
formatSize,
getMediaTypeFromFilename,
truncateFilename
} from '@/utils/formatUtil'
import { iconForJobState } from '@/queue/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
const { assets, isSelected } = defineProps<{
assets: AssetItem[]
isSelected: (assetId: string) => boolean
}>()
const emit = defineEmits<{
(e: 'select-asset', asset: AssetItem): void
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
(e: 'approach-end'): void
}>()
const { t } = useI18n()
const { jobItems } = useJobList()
const { getJobActions, runJobAction } = useJobActions()
const hoveredJobId = ref<string | null>(null)
const hoveredAssetId = ref<string | null>(null)
type AssetListItem = { key: string; asset: AssetItem }
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state))
)
const assetItems = computed<AssetListItem[]>(() =>
assets.map((asset) => ({
key: `asset-${asset.id}`,
asset
}))
)
const listGridStyle = {
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr)',
padding: '0 0.5rem',
gap: '0.5rem'
}
const listCardBaseClass =
'w-full text-text-primary transition-colors hover:bg-secondary-background-hover'
function isActiveJobState(state: JobState): boolean {
return (
state === 'pending' || state === 'initialization' || state === 'running'
)
}
function getAssetPrimaryText(asset: AssetItem): string {
return truncateFilename(asset.name)
}
function getAssetSecondaryText(asset: AssetItem): string {
const metadata = getOutputAssetMetadata(asset.user_metadata)
if (typeof metadata?.executionTimeInSeconds === 'number') {
return `${metadata.executionTimeInSeconds.toFixed(2)}s`
}
const duration = asset.user_metadata?.duration
if (typeof duration === 'number') {
return formatDuration(duration)
}
if (typeof asset.size === 'number') {
return formatSize(asset.size)
}
return ''
}
function getAssetIconName(asset: AssetItem): string {
const mediaType = getMediaTypeFromFilename(asset.name)
if (mediaType === 'video') return 'icon-[lucide--video]'
if (mediaType === 'audio') return 'icon-[lucide--music]'
if (mediaType === '3D') return 'icon-[lucide--box]'
return 'icon-[lucide--image]'
}
function getAssetCardClass(selected: boolean): string {
return cn(
listCardBaseClass,
'cursor-pointer',
selected &&
'bg-secondary-background-hover ring-1 ring-inset ring-modal-card-border-highlighted'
)
}
function getJobCardClass(): string {
return cn(listCardBaseClass, 'cursor-default')
}
function onJobEnter(jobId: string) {
hoveredJobId.value = jobId
}
function onJobLeave(jobId: string) {
if (hoveredJobId.value === jobId) {
hoveredJobId.value = null
}
}
function onAssetEnter(assetId: string) {
hoveredAssetId.value = assetId
}
function onAssetLeave(assetId: string) {
if (hoveredAssetId.value === assetId) {
hoveredAssetId.value = null
}
}
function getJobIconClass(job: JobListItem): string | undefined {
const classes = []
const iconName = job.iconName ?? iconForJobState(job.state)
if (!job.iconImageUrl && iconName === iconForJobState('pending')) {
classes.push('animate-spin')
}
return classes.length ? classes.join(' ') : undefined
}
function handleJobAction(action: JobAction, job: JobListItem) {
void runJobAction(action, job)
}
function handleAssetMenuClick(event: MouseEvent, asset: AssetItem) {
event.stopPropagation()
emit('context-menu', event, asset)
}
</script>

View File

@@ -47,17 +47,42 @@
<MediaAssetFilterBar
v-model:search-query="searchQuery"
v-model:sort-by="sortBy"
v-model:view-mode="viewMode"
v-model:media-type-filters="mediaTypeFilters"
class="pb-1 px-2 2xl:px-4"
:show-generation-time-sort="activeTab === 'output'"
/>
<Divider type="dashed" class="my-2" />
<div
v-if="isQueuePanelV2Enabled"
class="flex items-center justify-between px-2 py-2 2xl:px-4"
>
<span class="text-sm text-muted-foreground">
{{ activeJobsLabel }}
</span>
<div class="flex items-center gap-2">
<span class="text-sm text-base-foreground">
{{ t('sideToolbar.queueProgressOverlay.clearQueueTooltip') }}
</span>
<Button
variant="destructive"
size="icon"
:aria-label="
t('sideToolbar.queueProgressOverlay.clearQueueTooltip')
"
:disabled="queuedCount === 0"
@click="handleClearQueue"
>
<i class="icon-[lucide--list-x] size-4" />
</Button>
</div>
</div>
<Divider v-else type="dashed" class="my-2" />
</template>
<template #body>
<div v-if="loading && !displayAssets.length">
<div v-if="showLoadingState">
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
</div>
<div v-else-if="!loading && !displayAssets.length">
<div v-else-if="showEmptyState">
<NoResultsPlaceholder
icon="pi pi-info-circle"
:title="
@@ -71,7 +96,16 @@
/>
</div>
<div v-else class="relative size-full" @click="handleEmptySpaceClick">
<AssetsSidebarListView
v-if="isListView"
:assets="displayAssets"
:is-selected="isSelected"
@select-asset="handleAssetSelect"
@context-menu="handleAssetContextMenu"
@approach-end="handleApproachEnd"
/>
<VirtualGrid
v-else
:items="mediaAssetsWithKey"
:grid-style="{
display: 'grid',
@@ -87,13 +121,10 @@
:selected="isSelected(item.id)"
:show-output-count="shouldShowOutputCount(item)"
:output-count="getOutputCount(item)"
:show-delete-button="shouldShowDeleteButton"
:open-context-menu-id="openContextMenuId"
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
@context-menu-opened="openContextMenuId = item.id"
@context-menu="handleAssetContextMenu($event, item)"
/>
</template>
</VirtualGrid>
@@ -160,47 +191,71 @@
v-model:active-index="galleryActiveIndex"
:all-gallery-items="galleryItems"
/>
<MediaAssetContextMenu
v-if="contextMenuAsset"
ref="contextMenuRef"
:asset="contextMenuAsset"
:asset-type="contextMenuAssetType"
:file-kind="contextMenuFileKind"
:show-delete-button="shouldShowDeleteButton"
@zoom="handleContextMenuZoom"
@asset-deleted="refreshAssets"
/>
</template>
<script setup lang="ts">
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
import { Divider } from 'primevue'
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.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'
import TabList from '@/components/tab/TabList.vue'
import Button from '@/components/ui/button/Button.vue'
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.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 { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAssetFiltering'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { ResultItemImpl } from '@/stores/queueStore'
import { ResultItemImpl, useQueueStore } from '@/queue/stores/queueStore'
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
import { cn } from '@/utils/tailwindUtil'
const { t } = useI18n()
const commandStore = useCommandStore()
const queueStore = useQueueStore()
const settingStore = useSettingStore()
const activeTab = ref<'input' | 'output'>('output')
const folderPromptId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined)
const isInFolderView = computed(() => folderPromptId.value !== null)
// Track which asset's context menu is open (for single-instance context menu management)
const openContextMenuId = ref<string | null>(null)
const viewMode = ref<'list' | 'grid'>('grid')
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const isListView = computed(
() => isQueuePanelV2Enabled.value && viewMode.value === 'list'
)
const contextMenuRef = ref<InstanceType<typeof MediaAssetContextMenu>>()
const contextMenuAsset = ref<AssetItem | null>(null)
// 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)
@@ -209,6 +264,14 @@ const shouldShowDeleteButton = computed(() => {
return true
})
const contextMenuAssetType = computed(() =>
contextMenuAsset.value ? getAssetType(contextMenuAsset.value.tags) : 'input'
)
const contextMenuFileKind = computed<MediaKind>(() =>
getMediaTypeFromFilename(contextMenuAsset.value?.name ?? '')
)
const getOutputCount = (item: AssetItem): number => {
const count = item.user_metadata?.outputCount
return typeof count === 'number' && count > 0 ? count : 1
@@ -226,6 +289,15 @@ const formattedExecutionTime = computed(() => {
return formatDuration(folderExecutionTime.value * 1000)
})
const queuedCount = computed(() => queueStore.pendingTasks.length)
const activeJobsCount = computed(
() => queueStore.pendingTasks.length + queueStore.runningTasks.length
)
const activeJobsLabel = computed(
() =>
`${activeJobsCount.value} ${t('sideToolbar.queueProgressOverlay.activeJobsSuffix')}`
)
const toast = useToast()
const inputAssets = useMediaAssets('input')
@@ -300,6 +372,20 @@ const displayAssets = computed(() => {
return filteredAssets.value
})
const showLoadingState = computed(
() =>
loading.value &&
displayAssets.value.length === 0 &&
(!isListView.value || activeJobsCount.value === 0)
)
const showEmptyState = computed(
() =>
!loading.value &&
displayAssets.value.length === 0 &&
(!isListView.value || activeJobsCount.value === 0)
)
watch(displayAssets, (newAssets) => {
if (currentGalleryAssetId.value && galleryActiveIndex.value !== -1) {
const newIndex = newAssets.findIndex(
@@ -371,6 +457,18 @@ const handleAssetSelect = (asset: AssetItem) => {
handleAssetClick(asset, index, displayAssets.value)
}
function handleAssetContextMenu(event: MouseEvent, asset: AssetItem) {
contextMenuAsset.value = asset
void nextTick(() => {
contextMenuRef.value?.show(event)
})
}
function handleContextMenuZoom() {
if (!contextMenuAsset.value) return
handleZoomClick(contextMenuAsset.value)
}
const handleZoomClick = (asset: AssetItem) => {
const mediaType = getMediaTypeFromFilename(asset.name)
@@ -490,6 +588,11 @@ const handleDeleteSelected = async () => {
clearSelection()
}
const handleClearQueue = async () => {
if (queuedCount.value === 0) return
await commandStore.execute('Comfy.ClearPendingTasks')
}
const handleApproachEnd = useDebounceFn(async () => {
if (
activeTab.value === 'output' &&

View File

@@ -8,7 +8,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/queue/stores/queueStore'
const { result } = defineProps<{
result: ResultItemImpl

View File

@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, nextTick } from 'vue'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/queue/stores/queueStore'
import ResultGallery from './ResultGallery.vue'

View File

@@ -45,7 +45,7 @@ import Galleria from 'primevue/galleria'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import ComfyImage from '@/components/common/ComfyImage.vue'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/queue/stores/queueStore'
import ResultAudio from './ResultAudio.vue'
import ResultVideo from './ResultVideo.vue'

View File

@@ -10,7 +10,7 @@ import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useExtensionStore } from '@/stores/extensionStore'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/queue/stores/queueStore'
const props = defineProps<{
result: ResultItemImpl

View File

@@ -39,7 +39,7 @@ import { useLitegraphService } from '@/services/litegraphService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useQueueSettingsStore, useQueueStore } from '@/queue/stores/queueStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
@@ -63,7 +63,6 @@ import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelector
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
const moveSelectedNodesVersionAdded = '1.22.2'
export function useCoreCommands(): ComfyCommand[] {
const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
@@ -75,6 +74,7 @@ export function useCoreCommands(): ComfyCommand[] {
const executionStore = useExecutionStore()
const telemetry = useTelemetry()
const { staticUrls, buildDocsUrl } = useExternalLink()
const settingStore = useSettingStore()
const bottomPanelStore = useBottomPanelStore()
@@ -82,6 +82,14 @@ export function useCoreCommands(): ComfyCommand[] {
useSelectedLiteGraphItems()
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
function isQueuePanelV2Enabled() {
return settingStore.get('Comfy.Queue.QPOV2')
}
async function toggleQueuePanelV2() {
await settingStore.set('Comfy.Queue.QPOV2', !isQueuePanelV2Enabled())
}
const moveSelectedNodes = (
positionUpdater: (pos: Point, gridSize: number) => Point
) => {
@@ -1175,6 +1183,12 @@ export function useCoreCommands(): ComfyCommand[] {
await useWorkflowService().reloadCurrentWorkflow() // ensure changes take effect immediately
}
},
{
id: 'Comfy.ToggleQPOV2',
icon: 'pi pi-list',
label: 'Toggle Queue Panel V2',
function: toggleQueuePanelV2
},
{
id: 'Comfy.ToggleLinear',
icon: 'pi pi-database',

View File

@@ -260,6 +260,9 @@
"Comfy_ToggleLinear": {
"label": "toggle linear mode"
},
"Comfy_ToggleQPOV2": {
"label": "Toggle Queue Panel V2"
},
"Comfy_ToggleTheme": {
"label": "Toggle Theme (Dark/Light)"
},
@@ -324,4 +327,4 @@
"label": "Toggle Workflows Sidebar",
"tooltip": "Workflows"
}
}
}

View File

@@ -687,6 +687,7 @@
"noFilesFound": "No files found",
"noImportedFiles": "No imported files found",
"noGeneratedFiles": "No generated files found",
"generatedAssetsHeader": "Generated assets",
"noFilesFoundMessage": "Upload files or generate content to see them here",
"browseTemplates": "Browse example templates",
"openWorkflow": "Open workflow in local file system",
@@ -718,6 +719,8 @@
"colonPercent": ": {percent}",
"currentNode": "Current node:",
"viewAllJobs": "View all jobs",
"viewList": "List view",
"viewGrid": "Grid view",
"running": "running",
"preview": "Preview",
"interruptAll": "Interrupt all running jobs",
@@ -2449,4 +2452,4 @@
"recentReleases": "Recent releases",
"helpCenterMenu": "Help Center Menu"
}
}
}

View File

@@ -0,0 +1,133 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '@/components/ui/button/Button.vue'
import { iconForJobState } from '@/queue/utils/queueDisplay'
import AssetsListCard from './AssetsListCard.vue'
function centeredDecorator() {
return {
template: `
<div style="width: 380px;">
<story />
</div>
`
}
}
const meta: Meta<typeof AssetsListCard> = {
title: 'Platform/Assets/AssetsListCard',
component: AssetsListCard,
parameters: {
layout: 'centered'
},
decorators: [centeredDecorator]
}
export default meta
type Story = StoryObj<typeof meta>
const IMAGE_PREVIEW = '/assets/images/comfy-logo-single.svg'
const VIDEO_PREVIEW = '/assets/images/default-template.png'
export const PendingJob: Story = {
args: {
iconName: iconForJobState('pending'),
iconClass: 'animate-spin',
primaryText: 'In queue',
secondaryText: '8:59:30pm'
}
}
export const InitializationJob: Story = {
args: {
iconName: iconForJobState('initialization'),
primaryText: 'Initializing...',
secondaryText: '8:59:35pm'
}
}
export const RunningJob: Story = {
args: {
iconName: iconForJobState('running'),
primaryText: 'Total: 30%',
secondaryText: 'CLIP Text Encode: 70%',
progressTotalPercent: 30,
progressCurrentPercent: 70
}
}
export const RunningJobWithActions: Story = {
args: {
iconName: iconForJobState('running'),
primaryText: 'Total: 30%',
secondaryText: 'KSampler: 70%',
progressTotalPercent: 30,
progressCurrentPercent: 70
},
render: renderRunningJobWithActions
}
export const FailedJob: Story = {
args: {
iconName: iconForJobState('failed'),
iconClass: 'text-destructive-background',
iconWrapperClass: 'bg-modal-card-placeholder-background',
primaryText: 'Failed',
secondaryText: '8:59:30pm'
}
}
export const GeneratedImage: Story = {
args: {
previewUrl: IMAGE_PREVIEW,
previewAlt: 'image-032.png',
primaryText: 'image-032.png',
secondaryText: '1.84s'
}
}
export const GeneratedVideo: Story = {
args: {
previewUrl: VIDEO_PREVIEW,
previewAlt: 'clip-01.mp4',
primaryText: 'clip-01.mp4',
secondaryText: '2m 12s'
}
}
export const GeneratedAudio: Story = {
args: {
iconName: 'icon-[lucide--music]',
primaryText: 'soundtrack-01.mp3',
secondaryText: '3m 20s'
}
}
export const Generated3D: Story = {
args: {
iconName: 'icon-[lucide--box]',
primaryText: 'scene-01.glb',
secondaryText: '128 MB'
}
}
type AssetsListCardProps = InstanceType<typeof AssetsListCard>['$props']
function renderRunningJobWithActions(args: AssetsListCardProps) {
return {
components: { AssetsListCard, Button },
setup() {
return { args }
},
template: `
<AssetsListCard v-bind="args">
<template #actions>
<Button variant="destructive" size="icon" aria-label="Cancel">
<i class="icon-[lucide--x] size-4" />
</Button>
</template>
</AssetsListCard>
`
}
}

View File

@@ -0,0 +1,102 @@
<template>
<div class="relative flex items-center gap-2 overflow-hidden rounded-lg p-2">
<div
v-if="
progressTotalPercent !== undefined ||
progressCurrentPercent !== undefined
"
class="absolute inset-0"
>
<div
v-if="progressTotalPercent !== undefined"
class="pointer-events-none absolute inset-y-0 left-0 h-full bg-interface-panel-job-progress-primary transition-[width]"
:style="{ width: `${clampPercent(progressTotalPercent)}%` }"
/>
<div
v-if="progressCurrentPercent !== undefined"
class="pointer-events-none absolute inset-y-0 left-0 h-full bg-interface-panel-job-progress-secondary transition-[width]"
:style="{ width: `${clampPercent(progressCurrentPercent)}%` }"
/>
</div>
<div
:class="
cn(
'relative z-1 flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-sm bg-secondary-background',
iconWrapperClass
)
"
:aria-label="iconAriaLabel || undefined"
>
<img
v-if="previewUrl"
:src="previewUrl"
:alt="previewAlt"
class="size-full object-cover"
/>
<div v-else class="flex size-full items-center justify-center">
<i
aria-hidden="true"
:class="
cn(
iconName ?? 'icon-[lucide--image]',
'size-4 text-text-secondary',
iconClass
)
"
/>
</div>
</div>
<div class="relative z-1 flex min-w-0 flex-1 flex-col gap-1">
<div
v-if="$slots.primary || primaryText"
class="text-xs leading-none text-text-primary"
>
<slot name="primary">{{ primaryText }}</slot>
</div>
<div
v-if="$slots.secondary || secondaryText"
class="text-xs leading-none text-text-secondary"
>
<slot name="secondary">{{ secondaryText }}</slot>
</div>
</div>
<div v-if="$slots.actions" class="relative z-1 flex items-center gap-2">
<slot name="actions" />
</div>
</div>
</template>
<script setup lang="ts">
import { cn } from '@/utils/tailwindUtil'
const {
previewUrl,
previewAlt = '',
iconName,
iconAriaLabel,
iconClass,
iconWrapperClass,
primaryText,
secondaryText,
progressTotalPercent,
progressCurrentPercent
} = defineProps<{
previewUrl?: string
previewAlt?: string
iconName?: string
iconAriaLabel?: string
iconClass?: string
iconWrapperClass?: string
primaryText?: string
secondaryText?: string
progressTotalPercent?: number
progressCurrentPercent?: number
}>()
function clampPercent(value: number) {
return Math.min(100, Math.max(0, value))
}
</script>

View File

@@ -110,21 +110,10 @@
</CardBottom>
</template>
</CardContainer>
<MediaAssetContextMenu
v-if="asset"
ref="contextMenu"
:asset="asset"
:asset-type="assetType"
:file-kind="fileKind"
:show-delete-button="showDeleteButton"
@zoom="handleZoomClick"
@asset-deleted="emit('asset-deleted')"
/>
</template>
<script setup lang="ts">
import { useElementHover, whenever } from '@vueuse/core'
import { useElementHover } from '@vueuse/core'
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
import IconGroup from '@/components/button/IconGroup.vue'
@@ -141,7 +130,6 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import type { AssetItem } from '../schemas/assetSchema'
import type { MediaKind } from '../schemas/mediaAssetSchema'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
import MediaAssetContextMenu from './MediaAssetContextMenu.vue'
const mediaComponents = {
top: {
@@ -166,34 +154,22 @@ function getBottomComponent(kind: MediaKind) {
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
}
const {
asset,
loading,
selected,
showOutputCount,
outputCount,
showDeleteButton,
openContextMenuId
} = defineProps<{
const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
asset?: AssetItem
loading?: boolean
selected?: boolean
showOutputCount?: boolean
outputCount?: number
showDeleteButton?: boolean
openContextMenuId?: string | null
}>()
const emit = defineEmits<{
click: []
zoom: [asset: AssetItem]
'output-count-click': []
'asset-deleted': []
'context-menu-opened': []
'context-menu': [event: MouseEvent]
}>()
const cardContainerRef = ref<HTMLElement>()
const contextMenu = ref<InstanceType<typeof MediaAssetContextMenu>>()
const isVideoPlaying = ref(false)
const showVideoControls = ref(false)
@@ -302,15 +278,6 @@ const handleOutputCountClick = () => {
}
const handleContextMenu = (event: MouseEvent) => {
emit('context-menu-opened')
contextMenu.value?.show(event)
emit('context-menu', event)
}
// Close this context menu when another opens
whenever(
() => openContextMenuId && openContextMenuId !== asset?.id,
() => {
contextMenu.value?.hide()
}
)
</script>

View File

@@ -31,18 +31,26 @@
/>
</template>
</AssetSortButton>
<MediaAssetViewModeToggle
v-if="isQueuePanelV2Enabled"
v-model:view-mode="viewMode"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import SearchBox from '@/components/common/SearchBox.vue'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import MediaAssetFilterButton from './MediaAssetFilterButton.vue'
import MediaAssetFilterMenu from './MediaAssetFilterMenu.vue'
import AssetSortButton from './MediaAssetSortButton.vue'
import MediaAssetSortMenu from './MediaAssetSortMenu.vue'
import type { SortBy } from './MediaAssetSortMenu.vue'
import MediaAssetViewModeToggle from './MediaAssetViewModeToggle.vue'
const { showGenerationTimeSort = false } = defineProps<{
searchQuery: string
@@ -56,6 +64,12 @@ const emit = defineEmits<{
}>()
const sortBy = defineModel<SortBy>('sortBy', { required: true })
const viewMode = defineModel<'list' | 'grid'>('viewMode', { required: true })
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const handleSearchChange = (value: string | undefined) => {
emit('update:searchQuery', value ?? '')

View File

@@ -0,0 +1,54 @@
<template>
<div
class="inline-flex items-center gap-1 rounded-lg bg-secondary-background p-1"
role="group"
>
<Button
type="button"
variant="muted-textonly"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.viewList')"
:aria-pressed="viewMode === 'list'"
:class="
cn(
'rounded-lg',
viewMode === 'list'
? 'bg-secondary-background-selected text-text-primary hover:bg-secondary-background-selected'
: 'text-text-secondary hover:bg-secondary-background-hover'
)
"
@click="viewMode = 'list'"
>
<i class="icon-[lucide--table-of-contents] size-4" />
</Button>
<Button
type="button"
variant="muted-textonly"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.viewGrid')"
:aria-pressed="viewMode === 'grid'"
:class="
cn(
'rounded-lg',
viewMode === 'grid'
? 'bg-secondary-background-selected text-text-primary hover:bg-secondary-background-selected'
: 'text-text-secondary hover:bg-secondary-background-hover'
)
"
@click="viewMode = 'grid'"
>
<i class="icon-[lucide--layout-grid] size-4" />
</Button>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
const viewMode = defineModel<'list' | 'grid'>('viewMode', { required: true })
const { t } = useI18n()
</script>

View File

@@ -2,7 +2,7 @@ import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetContext } from '@/platform/assets/schemas/mediaAssetSchema'
import { api } from '@/scripts/api'
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
import type { ResultItemImpl, TaskItemImpl } from '@/queue/stores/queueStore'
/**
* Extract asset type from tags array

View File

@@ -1,12 +1,12 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ResultItemImpl } from '@/stores/queueStore'
import { ResultItemImpl } from '@/queue/stores/queueStore'
import type { AssetMeta } from '../schemas/mediaAssetSchema'
import { useMediaAssetGalleryStore } from './useMediaAssetGalleryStore'
vi.mock('@/stores/queueStore', () => ({
vi.mock('@/queue/stores/queueStore', () => ({
ResultItemImpl: vi
.fn<typeof ResultItemImpl>()
.mockImplementation(function (data) {

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { ref, shallowRef } from 'vue'
import { ResultItemImpl } from '@/stores/queueStore'
import { ResultItemImpl } from '@/queue/stores/queueStore'
import type { AssetMeta } from '../schemas/mediaAssetSchema'

View File

@@ -1,5 +1,5 @@
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/queue/stores/queueStore'
/**
* Metadata for output assets from queue store

View File

@@ -1139,5 +1139,13 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'hidden',
defaultValue: false,
versionAdded: '1.34.1'
},
{
id: 'Comfy.Queue.QPOV2',
name: 'Queue Panel V2',
type: 'hidden',
tooltip: 'Enable the new Assets Panel design with list/grid view toggle',
defaultValue: false,
experimental: true
}
]

View File

@@ -89,7 +89,7 @@ import Button from '@/components/ui/button/Button.vue'
import type {
CompletionSummary,
CompletionSummaryMode
} from '@/composables/queue/useCompletionSummary'
} from '@/queue/composables/useCompletionSummary'
type Props = {
mode: CompletionSummaryMode

View File

@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import QueueOverlayEmpty from './QueueOverlayEmpty.vue'
import type { CompletionSummary } from '@/composables/queue/useCompletionSummary'
import type { CompletionSummary } from '@/queue/composables/useCompletionSummary'
const i18n = createI18n({
legacy: false,

View File

@@ -14,8 +14,8 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import CompletionSummaryBanner from '@/components/queue/CompletionSummaryBanner.vue'
import type { CompletionSummary } from '@/composables/queue/useCompletionSummary'
import CompletionSummaryBanner from '@/queue/components/CompletionSummaryBanner.vue'
import type { CompletionSummary } from '@/queue/composables/useCompletionSummary'
defineProps<{ summary: CompletionSummary }>()

View File

@@ -79,9 +79,9 @@ import type {
JobListItem,
JobSortMode,
JobTab
} from '@/composables/queue/useJobList'
import type { MenuEntry } from '@/composables/queue/useJobMenu'
import { useJobMenu } from '@/composables/queue/useJobMenu'
} from '@/queue/composables/useJobList'
import type { MenuEntry } from '@/queue/composables/useJobMenu'
import { useJobMenu } from '@/queue/composables/useJobMenu'
import QueueOverlayHeader from './QueueOverlayHeader.vue'
import JobContextMenu from './job/JobContextMenu.vue'

View File

@@ -63,16 +63,16 @@
import { computed, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import QueueOverlayActive from '@/components/queue/QueueOverlayActive.vue'
import QueueOverlayEmpty from '@/components/queue/QueueOverlayEmpty.vue'
import QueueOverlayExpanded from '@/components/queue/QueueOverlayExpanded.vue'
import QueueClearHistoryDialog from '@/components/queue/dialogs/QueueClearHistoryDialog.vue'
import QueueOverlayActive from '@/queue/components/QueueOverlayActive.vue'
import QueueOverlayEmpty from '@/queue/components/QueueOverlayEmpty.vue'
import QueueOverlayExpanded from '@/queue/components/QueueOverlayExpanded.vue'
import QueueClearHistoryDialog from '@/queue/components/dialogs/QueueClearHistoryDialog.vue'
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
import { useCompletionSummary } from '@/composables/queue/useCompletionSummary'
import { useJobList } from '@/composables/queue/useJobList'
import type { JobListItem } from '@/composables/queue/useJobList'
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
import { useResultGallery } from '@/composables/queue/useResultGallery'
import { useCompletionSummary } from '@/queue/composables/useCompletionSummary'
import { useJobList } from '@/queue/composables/useJobList'
import type { JobListItem } from '@/queue/composables/useJobList'
import { useQueueProgress } from '@/queue/composables/useQueueProgress'
import { useResultGallery } from '@/queue/composables/useResultGallery'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useAssetSelectionStore } from '@/platform/assets/composables/useAssetSelectionStore'
import { isCloud } from '@/platform/distribution/types'
@@ -81,7 +81,7 @@ import { useAssetsStore } from '@/stores/assetsStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
type OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'

View File

@@ -53,7 +53,7 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useDialogStore } from '@/stores/dialogStore'
import { useQueueStore } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
const dialogStore = useDialogStore()
const queueStore = useQueueStore()

View File

@@ -47,7 +47,7 @@ import Popover from 'primevue/popover'
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import type { MenuEntry } from '@/composables/queue/useJobMenu'
import type { MenuEntry } from '@/queue/composables/useJobMenu'
defineProps<{ entries: MenuEntry[] }>()

View File

@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { TaskStatus } from '@/schemas/apiSchema'
import { useExecutionStore } from '@/stores/executionStore'
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import { TaskItemImpl, useQueueStore } from '@/queue/stores/queueStore'
import JobDetailsPopover from './JobDetailsPopover.vue'

View File

@@ -101,10 +101,10 @@ import { isCloud } from '@/platform/distribution/types'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useDialogService } from '@/services/dialogService'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
import type { TaskItemImpl } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import { formatClockTime } from '@/utils/dateTimeUtil'
import { jobStateFromTask } from '@/utils/queueUtil'
import { jobStateFromTask } from '@/queue/utils/queueUtil'
import { useJobErrorReporting } from './useJobErrorReporting'
import { formatElapsedTime, useQueueEstimates } from './useQueueEstimates'

View File

@@ -137,8 +137,8 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'
import type { JobSortMode, JobTab } from '@/composables/queue/useJobList'
import { jobSortModes, jobTabs } from '@/queue/composables/useJobList'
import type { JobSortMode, JobTab } from '@/queue/composables/useJobList'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { isCloud } from '@/platform/distribution/types'

View File

@@ -38,8 +38,8 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import QueueJobItem from '@/components/queue/job/QueueJobItem.vue'
import type { JobGroup, JobListItem } from '@/composables/queue/useJobList'
import QueueJobItem from '@/queue/components/job/QueueJobItem.vue'
import type { JobGroup, JobListItem } from '@/queue/composables/useJobList'
const props = defineProps<{ displayedJobGroups: JobGroup[] }>()

View File

@@ -198,12 +198,12 @@
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
import JobDetailsPopover from '@/queue/components/job/JobDetailsPopover.vue'
import QueueAssetPreview from '@/queue/components/job/QueueAssetPreview.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import type { JobState } from '@/types/queue'
import { iconForJobState } from '@/utils/queueDisplay'
import type { JobState } from '@/queue/types/queue'
import { iconForJobState } from '@/queue/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
const props = withDefaults(

View File

@@ -2,7 +2,7 @@ import { computed } from 'vue'
import type { ComputedRef } from 'vue'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import type { TaskItemImpl } from '@/stores/queueStore'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
type CopyHandler = (value: string) => void | Promise<void>

View File

@@ -1,8 +1,8 @@
import { computed, ref } from 'vue'
import { describe, expect, it } from 'vitest'
import type { TaskItemImpl } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import type { JobState } from '@/queue/types/queue'
import { formatElapsedTime, useQueueEstimates } from './useQueueEstimates'
import type { UseQueueEstimatesOptions } from './useQueueEstimates'

View File

@@ -2,8 +2,8 @@ import { computed } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import type { useExecutionStore } from '@/stores/executionStore'
import type { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
import type { TaskItemImpl, useQueueStore } from '@/queue/stores/queueStore'
import type { JobState } from '@/queue/types/queue'
type QueueStore = ReturnType<typeof useQueueStore>
type ExecutionStore = ReturnType<typeof useExecutionStore>

View File

@@ -1,8 +1,8 @@
import { computed, ref, watch } from 'vue'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
import { jobStateFromTask } from '@/utils/queueUtil'
import { useQueueStore } from '@/queue/stores/queueStore'
import { jobStateFromTask } from '@/queue/utils/queueUtil'
export type CompletionSummaryMode = 'allSuccess' | 'mixed' | 'allFailed'

View File

@@ -0,0 +1,56 @@
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { JobListItem } from '@/queue/composables/useJobList'
import { useJobMenu } from '@/queue/composables/useJobMenu'
import type { JobState } from '@/queue/types/queue'
type JobActionKey = 'cancel'
export type JobAction = {
key: JobActionKey
icon: string
label: string
variant: 'destructive' | 'secondary' | 'textonly'
}
export function useJobActions() {
const { t } = useI18n()
const { wrapWithErrorHandlingAsync } = useErrorHandling()
const currentJob = ref<JobListItem | null>(null)
const { cancelJob } = useJobMenu(() => currentJob.value)
const jobActionSets = computed<Partial<Record<JobState, JobAction[]>>>(() => {
const cancelAction: JobAction = {
key: 'cancel',
icon: 'icon-[lucide--x]',
label: t('sideToolbar.queueProgressOverlay.cancelJobTooltip'),
variant: 'destructive'
}
return {
pending: [cancelAction],
initialization: [cancelAction],
running: [cancelAction]
}
})
const getJobActions = (job: JobListItem): JobAction[] =>
job.showClear === false ? [] : (jobActionSets.value[job.state] ?? [])
const runJobAction = wrapWithErrorHandlingAsync(
async (action: JobAction, job: JobListItem) => {
currentJob.value = job
if (action.key === 'cancel') {
await cancelJob()
}
}
)
return {
getJobActions,
runJobAction
}
}

View File

@@ -1,14 +1,14 @@
import { computed, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
import { useQueueProgress } from '@/queue/composables/useQueueProgress'
import { st } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
import type { TaskItemImpl } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
import { useQueueStore } from '@/queue/stores/queueStore'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import type { JobState } from '@/queue/types/queue'
import {
dateKey,
formatClockTime,
@@ -17,8 +17,8 @@ import {
isYesterday
} from '@/utils/dateTimeUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildJobDisplay } from '@/utils/queueDisplay'
import { jobStateFromTask } from '@/utils/queueUtil'
import { buildJobDisplay } from '@/queue/utils/queueDisplay'
import { jobStateFromTask } from '@/queue/utils/queueUtil'
/** Tabs for job list filtering */
export const jobTabs = ['All', 'Completed', 'Failed'] as const

View File

@@ -1,7 +1,7 @@
import { computed } from 'vue'
import { downloadFile } from '@/base/common/downloadUtil'
import type { JobListItem } from '@/composables/queue/useJobList'
import type { JobListItem } from '@/queue/composables/useJobList'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { st, t } from '@/i18n'
import { mapTaskOutputToAssetItem } from '@/platform/assets/composables/media/assetMappers'
@@ -19,8 +19,8 @@ import { downloadBlob } from '@/scripts/utils'
import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useQueueStore } from '@/stores/queueStore'
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
import type { ResultItemImpl, TaskItemImpl } from '@/queue/stores/queueStore'
import { createAnnotatedPath } from '@/utils/createAnnotatedPath'
import { appendJsonExt } from '@/utils/formatUtil'

View File

@@ -1,7 +1,7 @@
import { ref, shallowRef } from 'vue'
import type { JobListItem } from '@/composables/queue/useJobList'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { JobListItem } from '@/queue/composables/useJobList'
import type { ResultItemImpl } from '@/queue/stores/queueStore'
/**
* Manages result gallery state and activation for queue items.

View File

@@ -3,7 +3,7 @@ import { app } from '@/scripts/app'
import {
useQueuePendingTaskCountStore,
useQueueSettingsStore
} from '@/stores/queueStore'
} from '@/queue/stores/queueStore'
export function setupAutoQueueHandler() {
const queueCountStore = useQueuePendingTaskCountStore()

View File

@@ -1,5 +1,5 @@
import type { TaskItemImpl } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import type { JobState } from '@/queue/types/queue'
import { formatDuration } from '@/utils/formatUtil'
import { clampPercentInt, formatPercent0 } from '@/utils/numberUtil'

View File

@@ -1,5 +1,5 @@
import type { TaskItemImpl } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import type { JobState } from '@/queue/types/queue'
/**
* Map a task to a UI job state, including initialization override.

View File

@@ -18,7 +18,7 @@ import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/compo
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
import { useQueueStore } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { AssetKind } from '@/types/widgetTypes'
import {

View File

@@ -491,6 +491,7 @@ const zSettings = z.object({
'Comfy.VueNodes.Enabled': z.boolean(),
'Comfy.VueNodes.AutoScaleLayout': z.boolean(),
'Comfy.Assets.UseAssetAPI': z.boolean(),
'Comfy.Queue.QPOV2': z.boolean(),
'Comfy-Desktop.AutoUpdate': z.boolean(),
'Comfy-Desktop.SendStatistics': z.boolean(),
'Comfy-Desktop.WindowStyle': z.string(),

View File

@@ -130,9 +130,6 @@ The following table lists ALL 46 store instances in the system as of 2025-09-01:
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
| releaseStore.ts | useReleaseStore | Manages application release information | System |
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
| settingStore.ts | useSettingStore | Manages application settings | Config |
@@ -148,6 +145,8 @@ The following table lists ALL 46 store instances in the system as of 2025-09-01:
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
Note: queue stores live in `src/queue/stores/queueStore.ts`.
### Workspace Stores
Located in `stores/workspace/`:
@@ -386,4 +385,4 @@ describe('useExampleStore', () => {
})
```
For more information on Pinia, refer to the [Pinia documentation](https://pinia.vuejs.org/introduction.html).
For more information on Pinia, refer to the [Pinia documentation](https://pinia.vuejs.org/introduction.html).

View File

@@ -11,7 +11,7 @@ import { isCloud } from '@/platform/distribution/types'
import type { TaskItem } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { TaskItemImpl } from './queueStore'
import { TaskItemImpl } from '@/queue/stores/queueStore'
const INPUT_LIMIT = 100

View File

@@ -12,7 +12,7 @@ import type { SidebarTabExtension, ToastManager } from '@/types/extensionTypes'
import { useApiKeyAuthStore } from './apiKeyAuthStore'
import { useCommandStore } from './commandStore'
import { useFirebaseAuthStore } from './firebaseAuthStore'
import { useQueueSettingsStore } from './queueStore'
import { useQueueSettingsStore } from '@/queue/stores/queueStore'
import { useBottomPanelStore } from './workspace/bottomPanelStore'
import { useSidebarTabStore } from './workspace/sidebarTabStore'

View File

@@ -0,0 +1,26 @@
import { ref } from 'vue'
import type { JobAction } from '@/queue/composables/useJobActions'
import type { JobListItem } from '@/queue/composables/useJobList'
const actionsByJobId = ref<Record<string, JobAction[]>>({})
export function setMockJobActions(actions: Record<string, JobAction[]>) {
actionsByJobId.value = actions
}
/** @knipIgnoreUnusedButUsedByStorybook */
export function useJobActions() {
function getJobActions(job: JobListItem) {
return actionsByJobId.value[job.id] ?? []
}
async function runJobAction() {
return undefined
}
return {
getJobActions,
runJobAction
}
}

View File

@@ -0,0 +1,59 @@
import { computed, ref } from 'vue'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import type {
JobGroup,
JobListItem,
JobSortMode,
JobTab
} from '@/queue/composables/useJobList'
const jobItems = ref<JobListItem[]>([])
function buildGroupedJobItems(): JobGroup[] {
return [
{
key: 'storybook',
label: 'Storybook',
items: jobItems.value
}
]
}
const groupedJobItems = computed<JobGroup[]>(buildGroupedJobItems)
const selectedJobTab = ref<JobTab>('All')
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
const selectedSortMode = ref<JobSortMode>('mostRecent')
const currentNodeName = ref('KSampler')
function buildEmptyTasks(): TaskItemImpl[] {
return []
}
const allTasksSorted = computed<TaskItemImpl[]>(buildEmptyTasks)
const filteredTasks = computed<TaskItemImpl[]>(buildEmptyTasks)
function buildHasFailedJobs() {
return jobItems.value.some((item) => item.state === 'failed')
}
const hasFailedJobs = computed(buildHasFailedJobs)
export function setMockJobItems(items: JobListItem[]) {
jobItems.value = items
}
/** @knipIgnoreUnusedButUsedByStorybook */
export function useJobList() {
return {
selectedJobTab,
selectedWorkflowFilter,
selectedSortMode,
hasFailedJobs,
allTasksSorted,
filteredTasks,
jobItems,
groupedJobItems,
currentNodeName
}
}

View File

@@ -58,7 +58,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
import { setupAutoQueueHandler } from '@/queue/services/autoQueueService'
import { useKeybindingService } from '@/services/keybindingService'
import { useAssetsStore } from '@/stores/assetsStore'
import { useCommandStore } from '@/stores/commandStore'
@@ -70,7 +70,7 @@ import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
import {
useQueuePendingTaskCountStore,
useQueueStore
} from '@/stores/queueStore'
} from '@/queue/stores/queueStore'
import { useServerConfigStore } from '@/stores/serverConfigStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'

View File

@@ -26,7 +26,7 @@ import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/compo
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useQueueSettingsStore } from '@/queue/stores/queueStore'
import { isElectron } from '@/utils/envUtil'
const nodeOutputStore = useNodeOutputStore()

View File

@@ -2,8 +2,8 @@ import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, nextTick } from 'vue'
import JobGroupsList from '@/components/queue/job/JobGroupsList.vue'
import type { JobGroup, JobListItem } from '@/composables/queue/useJobList'
import JobGroupsList from '@/queue/components/job/JobGroupsList.vue'
import type { JobGroup, JobListItem } from '@/queue/composables/useJobList'
const QueueJobItemStub = defineComponent({
name: 'QueueJobItemStub',
@@ -22,7 +22,6 @@ const QueueJobItemStub = defineComponent({
runningNodeName: { type: String, default: undefined },
activeDetailsId: { type: String, default: null }
},
emits: ['cancel', 'delete', 'menu', 'view', 'details-enter', 'details-leave'],
template: '<div class="queue-job-item-stub"></div>'
})

View File

@@ -3,12 +3,12 @@ import { computed, ref } from 'vue'
import type { ComputedRef } from 'vue'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import type { TaskItemImpl } from '@/stores/queueStore'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
import type {
JobErrorDialogService,
UseJobErrorReportingOptions
} from '@/components/queue/job/useJobErrorReporting'
import * as jobErrorReporting from '@/components/queue/job/useJobErrorReporting'
} from '@/queue/components/job/useJobErrorReporting'
import * as jobErrorReporting from '@/queue/components/job/useJobErrorReporting'
const createExecutionErrorMessage = (
overrides: Partial<ExecutionErrorWsMessage> = {}

View File

@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, reactive } from 'vue'
import { useCompletionSummary } from '@/composables/queue/useCompletionSummary'
import { useCompletionSummary } from '@/queue/composables/useCompletionSummary'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
import { useQueueStore } from '@/queue/stores/queueStore'
type MockTask = {
displayStatus: 'Completed' | 'Failed' | 'Cancelled' | 'Running' | 'Pending'
@@ -14,7 +14,7 @@ type MockTask = {
}
}
vi.mock('@/stores/queueStore', () => {
vi.mock('@/queue/stores/queueStore', () => {
const state = reactive({
runningTasks: [] as MockTask[],
historyTasks: [] as MockTask[]

View File

@@ -3,11 +3,11 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { nextTick, reactive, ref } from 'vue'
import type { Ref } from 'vue'
import { useJobList } from '@/composables/queue/useJobList'
import type { JobState } from '@/types/queue'
import { buildJobDisplay } from '@/utils/queueDisplay'
import type { BuildJobDisplayCtx } from '@/utils/queueDisplay'
import type { TaskItemImpl } from '@/stores/queueStore'
import { useJobList } from '@/queue/composables/useJobList'
import type { JobState } from '@/queue/types/queue'
import { buildJobDisplay } from '@/queue/utils/queueDisplay'
import type { BuildJobDisplayCtx } from '@/queue/utils/queueDisplay'
import type { TaskItemImpl } from '@/queue/stores/queueStore'
type TestTask = {
promptId: string
@@ -57,7 +57,7 @@ const ensureProgressRefs = () => {
if (!currentNodePercent) currentNodePercent = ref(0) as Ref<number>
return { totalPercent, currentNodePercent }
}
vi.mock('@/composables/queue/useQueueProgress', () => ({
vi.mock('@/queue/composables/useQueueProgress', () => ({
useQueueProgress: () => {
ensureProgressRefs()
return {
@@ -67,7 +67,7 @@ vi.mock('@/composables/queue/useQueueProgress', () => ({
}
}))
vi.mock('@/utils/queueDisplay', () => ({
vi.mock('@/queue/utils/queueDisplay', () => ({
buildJobDisplay: vi.fn(
(task: TaskItemImpl, state: JobState, options: BuildJobDisplayCtx) => ({
primary: `Job ${task.promptId}`,
@@ -80,7 +80,7 @@ vi.mock('@/utils/queueDisplay', () => ({
)
}))
vi.mock('@/utils/queueUtil', () => ({
vi.mock('@/queue/utils/queueUtil', () => ({
jobStateFromTask: vi.fn(
(task: TestTask, isInitializing?: boolean): JobState =>
task.mockState ?? (isInitializing ? 'running' : 'completed')
@@ -102,7 +102,7 @@ const ensureQueueStore = () => {
}
return queueStoreMock
}
vi.mock('@/stores/queueStore', () => ({
vi.mock('@/queue/stores/queueStore', () => ({
useQueueStore: () => {
return ensureQueueStore()
}

View File

@@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import type { Ref } from 'vue'
import type { JobListItem } from '@/composables/queue/useJobList'
import type { MenuEntry } from '@/composables/queue/useJobMenu'
import type { JobListItem } from '@/queue/composables/useJobList'
import type { MenuEntry } from '@/queue/composables/useJobMenu'
const downloadFileMock = vi.fn()
vi.mock('@/base/common/downloadUtil', () => ({
@@ -99,7 +99,7 @@ const queueStoreMock = {
update: vi.fn(),
delete: vi.fn()
}
vi.mock('@/stores/queueStore', () => ({
vi.mock('@/queue/stores/queueStore', () => ({
useQueueStore: () => queueStoreMock
}))
@@ -116,7 +116,7 @@ vi.mock('@/utils/formatUtil', () => ({
appendJsonExtMock(...args)
}))
import { useJobMenu } from '@/composables/queue/useJobMenu'
import { useJobMenu } from '@/queue/composables/useJobMenu'
const createJobItem = (overrides: Partial<JobListItem> = {}): JobListItem => ({
id: overrides.id ?? 'job-1',

View File

@@ -4,7 +4,7 @@ import { nextTick, ref } from 'vue'
import type { Ref } from 'vue'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
import { useQueueProgress } from '@/queue/composables/useQueueProgress'
import { formatPercent0 } from '@/utils/numberUtil'
type ProgressValue = number | null

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest'
import { useResultGallery } from '@/composables/queue/useResultGallery'
import type { JobListItem } from '@/composables/queue/useJobList'
import { useResultGallery } from '@/queue/composables/useResultGallery'
import type { JobListItem } from '@/queue/composables/useJobList'
type PreviewLike = { url: string; supportsPreview: boolean }

View File

@@ -22,7 +22,7 @@ const {
WidgetToggleSwitch
} = FOR_TESTING
vi.mock('@/stores/queueStore', () => ({
vi.mock('@/queue/stores/queueStore', () => ({
useQueueStore: vi.fn(() => ({
historyTasks: []
}))

View File

@@ -32,7 +32,7 @@ vi.mock('@/platform/distribution/types', () => ({
}))
// Mock TaskItemImpl
vi.mock('@/stores/queueStore', () => ({
vi.mock('@/queue/stores/queueStore', () => ({
TaskItemImpl: class {
public flatOutputs: Array<{
supportsPreview: boolean

View File

@@ -10,7 +10,7 @@ import type {
TaskStatus
} from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import { TaskItemImpl, useQueueStore } from '@/queue/stores/queueStore'
// Fixture factories
const createTaskPrompt = (

View File

@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyApp } from '@/scripts/app'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { TaskItemImpl } from '@/stores/queueStore'
import { TaskItemImpl } from '@/queue/stores/queueStore'
import * as getWorkflowModule from '@/platform/workflow/cloud'
vi.mock('@/platform/distribution/types', () => ({