[QPOv2] Add list view to assets sidepanel (#7737)

This adds the list view to the media assets sidepanel, while also adding
the active jobs to be displayed right now.

The design for this is actually changing, which is why it is in draft
right now. There are technical limitations of the virtual grid that
doesn't make it easy for both the active jobs and generated assets to
exist on the same container. Currently WIP right now.


Part of the QPO v2 iteration, figma design can be found
[here](https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3330-37286&m=dev).
This will be implemented in a series of stacked PRs that can be reviewed
and merged individually.

main <-- #7737, #7743, #7745

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7737-QPOv2-Add-list-view-to-assets-sidepanel-2d26d73d365081858e22c48902bd56e2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Benjamin Lu
2026-01-10 10:56:29 -08:00
committed by GitHub
parent f843d779c2
commit 8086f977c9
10 changed files with 650 additions and 71 deletions

View File

@@ -0,0 +1,99 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '@/components/ui/button/Button.vue'
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
const meta: Meta<typeof AssetsListItem> = {
title: 'Platform/Assets/AssetsListItem',
component: AssetsListItem,
parameters: {
layout: 'centered'
},
decorators: [
() => ({
template: '<div class="p-8 bg-base-background"><story /></div>'
})
]
}
export default meta
type Story = StoryObj<typeof meta>
type AssetsListItemProps = InstanceType<typeof AssetsListItem>['$props']
function renderActiveJob(args: AssetsListItemProps) {
return {
components: { Button, AssetsListItem },
setup() {
return { args }
},
template: `
<AssetsListItem v-bind="args">
<template #primary>
<div class="flex items-center gap-1 text-text-primary">
<span>Total:</span>
<span class="font-medium">30%</span>
</div>
</template>
<template #secondary>
<div class="flex items-center gap-1 text-text-secondary">
<span>CLIP Text Encode:</span>
<span>70%</span>
</div>
</template>
<template #actions>
<Button variant="destructive" size="icon" aria-label="Cancel">
<i class="icon-[lucide--x] size-4" />
</Button>
</template>
</AssetsListItem>
`
}
}
function renderGeneratedAsset(args: AssetsListItemProps) {
return {
components: { AssetsListItem },
setup() {
return { args }
},
template: `
<AssetsListItem v-bind="args">
<template #secondary>
<div class="flex items-center gap-2 text-text-secondary">
<span>1m 56s</span>
<span>512x512</span>
</div>
</template>
</AssetsListItem>
`
}
}
export const ActiveJob: Story = {
args: {
previewUrl: '/assets/images/comfy-logo-single.svg',
previewAlt: 'Job preview',
progressTotalPercent: 30,
progressCurrentPercent: 70
},
render: renderActiveJob
}
export const FailedJob: Story = {
args: {
iconName: 'icon-[lucide--circle-alert]',
iconClass: 'text-destructive-background',
iconWrapperClass: 'bg-modal-card-placeholder-background',
primaryText: 'Failed',
secondaryText: '8:59:30pm'
}
}
export const GeneratedAsset: Story = {
args: {
previewUrl: '/assets/images/comfy-logo-single.svg',
previewAlt: 'image03.png',
primaryText: 'image03.png'
},
render: renderGeneratedAsset
}

View File

@@ -0,0 +1,116 @@
<template>
<div
class="relative flex items-center gap-2 overflow-hidden rounded-lg p-2 select-none"
>
<div
v-if="hasAnyProgressPercent(progressTotalPercent, progressCurrentPercent)"
:class="progressBarContainerClass"
>
<div
v-if="hasProgressPercent(progressTotalPercent)"
:class="progressBarPrimaryClass"
:style="progressPercentStyle(progressTotalPercent)"
/>
<div
v-if="hasProgressPercent(progressCurrentPercent)"
:class="progressBarSecondaryClass"
:style="progressPercentStyle(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"
>
<slot
name="icon"
:preview-url="previewUrl"
:preview-alt="previewAlt"
:icon-name="iconName"
:icon-class="iconClass"
:icon-aria-label="iconAriaLabel"
>
<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>
</slot>
</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 { useProgressBarBackground } from '@/composables/useProgressBarBackground'
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
}>()
const {
progressBarContainerClass,
progressBarPrimaryClass,
progressBarSecondaryClass,
hasProgressPercent,
hasAnyProgressPercent,
progressPercentStyle
} = useProgressBarBackground()
</script>

View File

@@ -0,0 +1,14 @@
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
export function iconForMediaType(mediaType: MediaKind): string {
switch (mediaType) {
case 'video':
return 'icon-[lucide--video]'
case 'audio':
return 'icon-[lucide--music]'
case '3D':
return 'icon-[lucide--box]'
default:
return 'icon-[lucide--image]'
}
}