fix: show skeleton loading state in asset folder view

Replace empty "No generated files found" flash with skeleton cards
when opening a multi-output job's folder view. The async resolution
of outputs via `resolveOutputAssetItems` now tracks loading state
using VueUse's `useAsyncState`, and the skeleton count matches the
expected output count from metadata.

- Add shadcn/vue Skeleton component
- Replace ProgressSpinner with skeleton grid matching asset card layout
- Use `useAsyncState` for folder asset resolution
- Wire `folderLoading` into `showLoadingState` / `showEmptyState`

Amp-Thread-ID: https://ampcode.com/threads/T-019c71fd-6654-7410-a3e1-e6e9915c9a88
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-02-18 10:41:51 -08:00
parent 2900e5e52e
commit 44f8151ba7
2 changed files with 58 additions and 20 deletions

View File

@@ -79,8 +79,22 @@
<Divider v-else type="dashed" class="my-2" />
</template>
<template #body>
<div v-if="showLoadingState">
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
<div
v-if="showLoadingState"
class="grid gap-2 px-2"
:style="skeletonGridStyle"
>
<div
v-for="n in skeletonCount"
:key="`skeleton-${n}`"
class="flex flex-col gap-2 p-2"
>
<Skeleton class="aspect-square w-full rounded-lg" />
<div class="flex flex-col gap-1">
<Skeleton class="h-4 w-3/4" />
<Skeleton class="h-3 w-1/2" />
</div>
</div>
</div>
<div v-else-if="showEmptyState">
<NoResultsPlaceholder
@@ -206,6 +220,7 @@
<script setup lang="ts">
import {
useAsyncState,
useDebounceFn,
useElementHover,
useResizeObserver,
@@ -213,7 +228,6 @@ import {
} from '@vueuse/core'
import { storeToRefs } from 'pinia'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -225,6 +239,7 @@ const Load3dViewerContent = () =>
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
import Skeleton from '@/components/ui/skeleton/Skeleton.vue'
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
@@ -237,6 +252,7 @@ import { useAssetSelection } from '@/platform/assets/composables/useAssetSelecti
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAssetFiltering'
import { useOutputStacks } from '@/platform/assets/composables/useOutputStacks'
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
@@ -260,6 +276,7 @@ const settingStore = useSettingStore()
const activeTab = ref<'input' | 'output'>('output')
const folderPromptId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined)
const expectedFolderCount = ref(0)
const isInFolderView = computed(() => folderPromptId.value !== null)
const viewMode = useStorage<'list' | 'grid'>(
'Comfy.Assets.Sidebar.ViewMode',
@@ -376,7 +393,26 @@ const mediaAssets = computed(() => currentAssets.value.media.value)
const galleryActiveIndex = ref(-1)
const currentGalleryAssetId = ref<string | null>(null)
const folderAssets = ref<AssetItem[]>([])
const skeletonGridStyle = {
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))'
}
const DEFAULT_SKELETON_COUNT = 6
const skeletonCount = computed(() =>
expectedFolderCount.value > 0
? expectedFolderCount.value
: DEFAULT_SKELETON_COUNT
)
const {
state: folderAssets,
isLoading: folderLoading,
execute: loadFolderAssets
} = useAsyncState(
(metadata: OutputAssetMetadata, options: { createdAt?: string } = {}) =>
resolveOutputAssetItems(metadata, options),
[] as AssetItem[],
{ immediate: false, resetOnExecute: true }
)
// Base assets before search filtering
const baseAssets = computed(() => {
@@ -416,7 +452,7 @@ const isBulkMode = computed(
const showLoadingState = computed(
() =>
loading.value &&
(loading.value || folderLoading.value) &&
displayAssets.value.length === 0 &&
activeJobsCount.value === 0
)
@@ -424,6 +460,7 @@ const showLoadingState = computed(
const showEmptyState = computed(
() =>
!loading.value &&
!folderLoading.value &&
displayAssets.value.length === 0 &&
activeJobsCount.value === 0
)
@@ -599,27 +636,15 @@ const enterFolderView = async (asset: AssetItem) => {
folderPromptId.value = promptId
folderExecutionTime.value = executionTimeInSeconds
expectedFolderCount.value = metadata.outputCount ?? 0
let folderItems: AssetItem[] = []
try {
folderItems = await resolveOutputAssetItems(metadata, {
createdAt: asset.created_at
})
} catch (error) {
console.error('Failed to resolve outputs for folder view:', error)
}
if (folderItems.length === 0) {
console.warn('No outputs available for folder view')
return
}
folderAssets.value = folderItems
await loadFolderAssets(0, metadata, { createdAt: asset.created_at })
}
const exitFolderView = () => {
folderPromptId.value = null
folderExecutionTime.value = undefined
expectedFolderCount.value = 0
folderAssets.value = []
searchQuery.value = ''
}

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const { class: className } = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('animate-pulse rounded-md bg-primary/10', className)" />
</template>