feat: split job history into a dedicated sidebar tab (#8957)

## Summary

Move queue job history into a dedicated sidebar tab (gated by `Comfy.Queue.QPOV2`) and remove mixed job-history UI from the Assets sidebar so assets and job controls are separated.

## Changes

- **What**:
  - Added `JobHistorySidebarTab` with reusable job UI primitives: `JobFilterTabs`, `JobFilterActions`, `JobAssetsList`, and shared `JobHistoryActionsMenu`.
  - Added reactive `job-history` tab registration in `sidebarTabStore`; prepends above Assets when `Comfy.Queue.QPOV2` is enabled and unregisters cleanly when disabled.
  - Added debounced search to `useJobList` (filters by job title, metadata, and prompt id).
  - Extracted clear-history dialog logic to `useQueueClearHistoryDialog` and reused it from queue overlay and job history tab.
  - Removed active-job rendering and queue-clear controls from assets list/grid/tab views; assets sidebar now focuses on media assets only.
  - Removed the QPOV2 gate from `MediaAssetViewModeToggle` and updated queue/job localized copy.
  - Added and updated tests for queue overlay header actions, job filters, search filtering, sidebar tab registration, and assets sidebar behavior.

## Review Focus

- Verify QPOV2 toggle behavior:
  - `Docked Job History` menu action toggles `Comfy.Queue.QPOV2`.
  - `job-history` tab insertion/removal order and active-tab reset on removal.
- Verify behavior split between tabs:
  - Job controls (cancel/delete/view/filter/search/clear history/clear queue) live in Job History.
  - Assets sidebar loading/empty states and list/grid rendering remain correct after removing active jobs.

## Screenshots (if applicable)
<img width="670" height="707" alt="image" src="https://github.com/user-attachments/assets/3a201fcb-d104-4e95-b5fe-49c4006a30a5" />
This commit is contained in:
Benjamin Lu
2026-02-20 16:42:41 -08:00
committed by GitHub
parent 7baa14af86
commit b3aed9afd0
27 changed files with 1194 additions and 662 deletions

View File

@@ -1,48 +1,6 @@
<template>
<div class="flex h-full flex-col">
<div
v-if="isQueuePanelV2Enabled && activeJobItems.length"
class="flex max-h-[50%] scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
>
<AssetsListItem
v-for="job in activeJobItems"
:key="job.id"
:class="
cn(
'w-full shrink-0 text-text-primary transition-colors hover:bg-secondary-background-hover',
'cursor-default'
)
"
: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" #actions>
<Button
v-if="canCancelJob"
:variant="cancelAction.variant"
size="icon"
:aria-label="cancelAction.label"
@click.stop="runCancelJob()"
>
<i :class="cancelAction.icon" class="size-4" />
</Button>
</template>
</AssetsListItem>
</div>
<div
v-if="assetItems.length"
:class="cn('px-2', activeJobItems.length && 'mt-2')"
>
<div v-if="assetItems.length" class="px-2">
<div
class="flex items-center p-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
>
@@ -119,31 +77,25 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Button from '@/components/ui/button/Button.vue'
import { useJobActions } from '@/composables/queue/useJobActions'
import type { JobListItem } from '@/composables/queue/useJobList'
import { useJobList } from '@/composables/queue/useJobList'
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
import { useAssetsStore } from '@/stores/assetsStore'
import { isActiveJobState } from '@/utils/queueUtil'
import {
formatDuration,
formatSize,
getMediaTypeFromFilename,
truncateFilename
} from '@/utils/formatUtil'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
import { useSettingStore } from '@/platform/settings/settingStore'
const {
assetItems,
@@ -170,24 +122,7 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const { jobItems } = useJobList()
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const hoveredJobId = ref<string | null>(null)
const hoveredAssetId = ref<string | null>(null)
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
)
const hoveredJob = computed(() =>
hoveredJobId.value
? (activeJobItems.value.find((job) => job.id === hoveredJobId.value) ??
null)
: null
)
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
const listGridStyle = {
display: 'grid',
@@ -240,16 +175,6 @@ function getAssetCardClass(selected: boolean): string {
)
}
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
}
@@ -259,13 +184,4 @@ function onAssetLeave(assetId: string) {
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
}
</script>