mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
fix: enhance loading states with skeletons and async state
This commit is contained in:
@@ -98,16 +98,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<!-- Loading State -->
|
<!-- No Results State (only show when loaded and no results) -->
|
||||||
<div v-if="isLoading" class="flex items-center justify-center h-64">
|
|
||||||
<div class="text-neutral-500">
|
|
||||||
{{ $t('templateWorkflows.loading', 'Loading templates...') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No Results State -->
|
|
||||||
<div
|
<div
|
||||||
v-else-if="filteredTemplates.length === 0 && !isLoading"
|
v-if="!isLoading && filteredTemplates.length === 0"
|
||||||
class="flex flex-col items-center justify-center h-64 text-neutral-500"
|
class="flex flex-col items-center justify-center h-64 text-neutral-500"
|
||||||
>
|
>
|
||||||
<i-lucide:search class="w-12 h-12 mb-4 opacity-50" />
|
<i-lucide:search class="w-12 h-12 mb-4 opacity-50" />
|
||||||
@@ -126,8 +119,11 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="px-6 pt-4 pb-2 text-2xl font-semibold text-neutral">
|
<div class="px-6 pt-4 pb-2 text-2xl font-semibold text-neutral">
|
||||||
<!-- show selected nav -->
|
<span
|
||||||
<span>
|
v-if="isLoading"
|
||||||
|
class="inline-block h-8 w-48 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse"
|
||||||
|
></span>
|
||||||
|
<span v-else>
|
||||||
{{ pageTitle }}
|
{{ pageTitle }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,8 +133,40 @@
|
|||||||
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-x-4 gap-y-6 px-4 py-4"
|
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-x-4 gap-y-6 px-4 py-4"
|
||||||
data-testid="template-workflows-content"
|
data-testid="template-workflows-content"
|
||||||
>
|
>
|
||||||
|
<!-- Loading Skeletons (show while loading initial data) -->
|
||||||
<CardContainer
|
<CardContainer
|
||||||
v-for="template in displayTemplates"
|
v-for="n in isLoading ? 12 : 0"
|
||||||
|
:key="`initial-skeleton-${n}`"
|
||||||
|
ratio="square"
|
||||||
|
:max-width="300"
|
||||||
|
:min-width="200"
|
||||||
|
>
|
||||||
|
<template #top>
|
||||||
|
<CardTop ratio="landscape">
|
||||||
|
<template #default>
|
||||||
|
<div
|
||||||
|
class="w-full h-full bg-neutral-200 dark-theme:bg-neutral-700 animate-pulse"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</CardTop>
|
||||||
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<CardBottom>
|
||||||
|
<div class="px-4 py-3">
|
||||||
|
<div
|
||||||
|
class="h-6 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse mb-2"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="h-4 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</CardBottom>
|
||||||
|
</template>
|
||||||
|
</CardContainer>
|
||||||
|
|
||||||
|
<!-- Actual Template Cards -->
|
||||||
|
<CardContainer
|
||||||
|
v-for="template in isLoading ? [] : displayTemplates"
|
||||||
:key="template.name"
|
:key="template.name"
|
||||||
ref="cardRefs"
|
ref="cardRefs"
|
||||||
v-memo="[template.name, hoveredTemplate === template.name]"
|
v-memo="[template.name, hoveredTemplate === template.name]"
|
||||||
@@ -355,8 +383,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useAsyncState } from '@vueuse/core'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, provide, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||||
@@ -420,8 +449,36 @@ const openTutorial = (template: TemplateInfo) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get navigation items from the store
|
// Get navigation items from the store, with skeleton items while loading
|
||||||
const navItems = computed<(NavItemData | NavGroupData)[]>(() => {
|
const navItems = computed<(NavItemData | NavGroupData)[]>(() => {
|
||||||
|
// Show skeleton navigation items while loading
|
||||||
|
if (isLoading.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'skeleton-all',
|
||||||
|
label: 'All Templates',
|
||||||
|
icon: 'icon-[lucide--layout-grid]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'skeleton-basics',
|
||||||
|
label: 'Basics',
|
||||||
|
icon: 'icon-[lucide--graduation-cap]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Generation Type',
|
||||||
|
items: [
|
||||||
|
{ id: 'skeleton-1', label: '...', icon: 'icon-[lucide--loader-2]' },
|
||||||
|
{ id: 'skeleton-2', label: '...', icon: 'icon-[lucide--loader-2]' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Closed Source Models',
|
||||||
|
items: [
|
||||||
|
{ id: 'skeleton-3', label: '...', icon: 'icon-[lucide--loader-2]' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
return workflowTemplatesStore.navGroupedTemplates
|
return workflowTemplatesStore.navGroupedTemplates
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -490,7 +547,6 @@ const selectedLicenseObjects = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
const isLoading = ref(true)
|
|
||||||
const loadingTemplate = ref<string | null>(null)
|
const loadingTemplate = ref<string | null>(null)
|
||||||
const hoveredTemplate = ref<string | null>(null)
|
const hoveredTemplate = ref<string | null>(null)
|
||||||
const cardRefs = ref<HTMLElement[]>([])
|
const cardRefs = ref<HTMLElement[]>([])
|
||||||
@@ -663,12 +719,22 @@ const pageTitle = computed(() => {
|
|||||||
t('templateWorkflows.allTemplates', 'All Templates')
|
t('templateWorkflows.allTemplates', 'All Templates')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize
|
// Initialize templates loading with useAsyncState
|
||||||
onMounted(async () => {
|
const { isLoading } = useAsyncState(
|
||||||
await loadTemplates()
|
async () => {
|
||||||
await workflowTemplatesStore.loadWorkflowTemplates()
|
// Run both operations in parallel for better performance
|
||||||
isLoading.value = false
|
await Promise.all([
|
||||||
})
|
loadTemplates(),
|
||||||
|
workflowTemplatesStore.loadWorkflowTemplates()
|
||||||
|
])
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
false, // initial state
|
||||||
|
{
|
||||||
|
immediate: true // Start loading immediately
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
cardRefs.value = [] // Release DOM refs
|
cardRefs.value = [] // Release DOM refs
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user