mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
Add stories for list view and general job card
This commit is contained in:
@@ -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
|
||||
|
||||
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 '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { setMockJobActions } from '@/storybook/mocks/useJobActions'
|
||||
import { setMockJobItems } from '@/storybook/mocks/useJobList'
|
||||
import { iconForJobState } from '@/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>
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import AssetsListCard from '@/platform/assets/components/AssetsListCard.vue'
|
||||
import { iconForJobState } from '@/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',
|
||||
@@ -9,37 +21,107 @@ const meta: Meta<typeof AssetsListCard> = {
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div class="p-8 bg-base-background"><story /></div>'
|
||||
})
|
||||
]
|
||||
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 renderActiveJob(args: AssetsListCardProps) {
|
||||
function renderRunningJobWithActions(args: AssetsListCardProps) {
|
||||
return {
|
||||
components: { Button, AssetsListCard },
|
||||
components: { AssetsListCard, Button },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<AssetsListCard 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" />
|
||||
@@ -49,51 +131,3 @@ function renderActiveJob(args: AssetsListCardProps) {
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
function renderGeneratedAsset(args: AssetsListCardProps) {
|
||||
return {
|
||||
components: { AssetsListCard },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<AssetsListCard v-bind="args">
|
||||
<template #secondary>
|
||||
<div class="flex items-center gap-2 text-text-secondary">
|
||||
<span>1m 56s</span>
|
||||
<span>512x512</span>
|
||||
</div>
|
||||
</template>
|
||||
</AssetsListCard>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
25
src/storybook/mocks/useJobActions.ts
Normal file
25
src/storybook/mocks/useJobActions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { JobAction } from '../../composables/queue/useJobActions'
|
||||
import type { JobListItem } from '../../composables/queue/useJobList'
|
||||
|
||||
const actionsByJobId = ref<Record<string, JobAction[]>>({})
|
||||
|
||||
export function setMockJobActions(actions: Record<string, JobAction[]>) {
|
||||
actionsByJobId.value = actions
|
||||
}
|
||||
|
||||
export function useJobActions() {
|
||||
function getJobActions(job: JobListItem) {
|
||||
return actionsByJobId.value[job.id] ?? []
|
||||
}
|
||||
|
||||
async function runJobAction() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
getJobActions,
|
||||
runJobAction
|
||||
}
|
||||
}
|
||||
58
src/storybook/mocks/useJobList.ts
Normal file
58
src/storybook/mocks/useJobList.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { TaskItemImpl } from '../../stores/queueStore'
|
||||
import type {
|
||||
JobGroup,
|
||||
JobListItem,
|
||||
JobSortMode,
|
||||
JobTab
|
||||
} from '../../composables/queue/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
|
||||
}
|
||||
|
||||
export function useJobList() {
|
||||
return {
|
||||
selectedJobTab,
|
||||
selectedWorkflowFilter,
|
||||
selectedSortMode,
|
||||
hasFailedJobs,
|
||||
allTasksSorted,
|
||||
filteredTasks,
|
||||
jobItems,
|
||||
groupedJobItems,
|
||||
currentNodeName
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user