mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 22:34:15 +00:00
fix: show skeleton loading state in asset folder view (#8979)
## Description When clicking a multi-output job to enter folder view, `resolveOutputAssetItems` fetches job details asynchronously. During this fetch, the panel showed "No generated files found" because there was no loading state for the folder resolution—only the media list fetch had one. This replaces the empty state flash with skeleton cards that match the asset grid layout, using the known output count from metadata to render the correct number of placeholders. Supersedes #8960. ### Changes - **Add shadcn/vue `Skeleton` component** (`src/components/ui/skeleton/Skeleton.vue`) - **Use `useAsyncState`** from VueUse to track folder asset resolution, providing `isLoading` automatically - **Wire `folderLoading`** into `showLoadingState` and `showEmptyState` computeds - **Replace `ProgressSpinner`** with a skeleton grid that mirrors the asset card layout - **Use `metadata.outputCount`** to predict skeleton count; falls back to 6 ### Before / After | Before | After | |--------|-------| | "No generated files found" flash | Skeleton cards matching grid layout | ## Checklist - [x] Code follows project conventions - [x] No `any` types introduced - [x] Lint and typecheck pass ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8979-fix-show-skeleton-loading-state-in-asset-folder-view-30c6d73d365081fa9809f616204ed234) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -79,8 +79,21 @@
|
||||
<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 grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2 px-2"
|
||||
>
|
||||
<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 +219,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useAsyncState,
|
||||
useDebounceFn,
|
||||
useElementHover,
|
||||
useResizeObserver,
|
||||
@@ -213,7 +227,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 +238,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 +251,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 +275,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 +392,24 @@ const mediaAssets = computed(() => currentAssets.value.media.value)
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const currentGalleryAssetId = ref<string | null>(null)
|
||||
|
||||
const folderAssets = ref<AssetItem[]>([])
|
||||
const DEFAULT_SKELETON_COUNT = 6
|
||||
const skeletonCount = computed(() =>
|
||||
expectedFolderCount.value > 0
|
||||
? expectedFolderCount.value
|
||||
: DEFAULT_SKELETON_COUNT
|
||||
)
|
||||
|
||||
const {
|
||||
state: folderAssets,
|
||||
isLoading: folderLoading,
|
||||
error: folderError,
|
||||
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(() => {
|
||||
@@ -414,9 +447,13 @@ const isBulkMode = computed(
|
||||
() => hasSelection.value && selectedAssets.value.length > 1
|
||||
)
|
||||
|
||||
const isFolderLoading = computed(
|
||||
() => isInFolderView.value && folderLoading.value
|
||||
)
|
||||
|
||||
const showLoadingState = computed(
|
||||
() =>
|
||||
loading.value &&
|
||||
(loading.value || isFolderLoading.value) &&
|
||||
displayAssets.value.length === 0 &&
|
||||
activeJobsCount.value === 0
|
||||
)
|
||||
@@ -424,6 +461,7 @@ const showLoadingState = computed(
|
||||
const showEmptyState = computed(
|
||||
() =>
|
||||
!loading.value &&
|
||||
!isFolderLoading.value &&
|
||||
displayAssets.value.length === 0 &&
|
||||
activeJobsCount.value === 0
|
||||
)
|
||||
@@ -599,27 +637,25 @@ 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
|
||||
await loadFolderAssets(0, metadata, { createdAt: asset.created_at })
|
||||
|
||||
if (folderError.value) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('sideToolbar.folderView.errorSummary'),
|
||||
detail: t('sideToolbar.folderView.errorDetail'),
|
||||
life: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve outputs for folder view:', error)
|
||||
exitFolderView()
|
||||
}
|
||||
|
||||
if (folderItems.length === 0) {
|
||||
console.warn('No outputs available for folder view')
|
||||
return
|
||||
}
|
||||
|
||||
folderAssets.value = folderItems
|
||||
}
|
||||
|
||||
const exitFolderView = () => {
|
||||
folderPromptId.value = null
|
||||
folderExecutionTime.value = undefined
|
||||
expectedFolderCount.value = 0
|
||||
folderAssets.value = []
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
15
src/components/ui/skeleton/Skeleton.vue
Normal file
15
src/components/ui/skeleton/Skeleton.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<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-secondary-background', className)"
|
||||
/>
|
||||
</template>
|
||||
@@ -743,6 +743,10 @@
|
||||
"filterText": "Text"
|
||||
},
|
||||
"backToAssets": "Back to all assets",
|
||||
"folderView": {
|
||||
"errorSummary": "Failed to load outputs",
|
||||
"errorDetail": "Could not retrieve outputs for this job. Please try again."
|
||||
},
|
||||
"searchAssets": "Search Assets",
|
||||
"labels": {
|
||||
"queue": "Queue",
|
||||
|
||||
Reference in New Issue
Block a user