mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
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:
@@ -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 = ''
|
||||
}
|
||||
|
||||
13
src/components/ui/skeleton/Skeleton.vue
Normal file
13
src/components/ui/skeleton/Skeleton.vue
Normal 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>
|
||||
Reference in New Issue
Block a user