mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
20 Commits
pysssss/dy
...
bl-move-qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003e72406d | ||
|
|
5561efcfd3 | ||
|
|
79af71530d | ||
|
|
f78b6eec47 | ||
|
|
d8e57c60bf | ||
|
|
db3edd522d | ||
|
|
f128c61c53 | ||
|
|
afa4664ad5 | ||
|
|
627db6784e | ||
|
|
85c6825a79 | ||
|
|
f614914fdf | ||
|
|
da4889900e | ||
|
|
5b1456896b | ||
|
|
0c6ea56360 | ||
|
|
2fd9a73b68 | ||
|
|
dc53cbe3c9 | ||
|
|
3d0c0d16ca | ||
|
|
a0dad31e2f | ||
|
|
5ddea4e7b6 | ||
|
|
94884d7a7c |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,7 +66,8 @@ const config: KnipConfig = {
|
||||
},
|
||||
tags: [
|
||||
'-knipIgnoreUnusedButUsedByCustomNodes',
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch'
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch',
|
||||
'-knipIgnoreUnusedButUsedByStorybook'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
155
src/components/sidebar/tabs/AssetsSidebarListView.stories.ts
Normal file
155
src/components/sidebar/tabs/AssetsSidebarListView.stories.ts
Normal 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>
|
||||
`
|
||||
}
|
||||
}
|
||||
241
src/components/sidebar/tabs/AssetsSidebarListView.vue
Normal file
241
src/components/sidebar/tabs/AssetsSidebarListView.vue
Normal 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>
|
||||
@@ -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' &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
133
src/platform/assets/components/AssetsListCard.stories.ts
Normal file
133
src/platform/assets/components/AssetsListCard.stories.ts
Normal 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>
|
||||
`
|
||||
}
|
||||
}
|
||||
102
src/platform/assets/components/AssetsListCard.vue
Normal file
102
src/platform/assets/components/AssetsListCard.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?? '')
|
||||
|
||||
54
src/platform/assets/components/MediaAssetViewModeToggle.vue
Normal file
54
src/platform/assets/components/MediaAssetViewModeToggle.vue
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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 }>()
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
@@ -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[] }>()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
|
||||
@@ -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[] }>()
|
||||
|
||||
@@ -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(
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
56
src/queue/composables/useJobActions.ts
Normal file
56
src/queue/composables/useJobActions.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
26
src/storybook/mocks/useJobActions.ts
Normal file
26
src/storybook/mocks/useJobActions.ts
Normal 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
|
||||
}
|
||||
}
|
||||
59
src/storybook/mocks/useJobList.ts
Normal file
59
src/storybook/mocks/useJobList.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>'
|
||||
})
|
||||
|
||||
|
||||
@@ -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> = {}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const {
|
||||
WidgetToggleSwitch
|
||||
} = FOR_TESTING
|
||||
|
||||
vi.mock('@/stores/queueStore', () => ({
|
||||
vi.mock('@/queue/stores/queueStore', () => ({
|
||||
useQueueStore: vi.fn(() => ({
|
||||
historyTasks: []
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
Reference in New Issue
Block a user