mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 01:20:09 +00:00
[Manager] Use skeleton placeholder when loading (#3150)
This commit is contained in:
@@ -35,9 +35,9 @@
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex justify-center items-center h-full"
|
||||
class="w-full h-full overflow-auto scrollbar-hide"
|
||||
>
|
||||
<ProgressSpinner />
|
||||
<GridSkeleton :grid-style="GRID_STYLE" :skeleton-card-count />
|
||||
</div>
|
||||
<NoResultsPlaceholder
|
||||
v-else-if="searchResults.length === 0"
|
||||
@@ -56,12 +56,7 @@
|
||||
<VirtualGrid
|
||||
:items="resultsWithKeys"
|
||||
:buffer-rows="3"
|
||||
:gridStyle="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(19rem, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '1.5rem'
|
||||
}"
|
||||
:gridStyle="GRID_STYLE"
|
||||
@approach-end="onApproachEnd"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
@@ -97,7 +92,6 @@
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -109,6 +103,7 @@ import InfoPanel from '@/components/dialog/content/manager/infoPanel/InfoPanel.v
|
||||
import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue'
|
||||
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
|
||||
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
|
||||
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
|
||||
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
@@ -127,6 +122,13 @@ enum ManagerTab {
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
const GRID_STYLE = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(19rem, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '1.5rem'
|
||||
} as const
|
||||
|
||||
const {
|
||||
isSmallScreen,
|
||||
isOpen: isSideNavOpen,
|
||||
@@ -243,8 +245,7 @@ watch(searchResults, onResultsChange, { flush: 'pre' })
|
||||
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
|
||||
|
||||
const isLoading = computed(() => {
|
||||
if (isSearchLoading.value)
|
||||
return searchResults.value.length === 0 || isInitialLoad.value
|
||||
if (isSearchLoading.value) return searchResults.value.length === 0
|
||||
if (selectedTab.value?.id === ManagerTab.Installed) {
|
||||
return isLoadingInstalled.value
|
||||
}
|
||||
@@ -254,7 +255,7 @@ const isLoading = computed(() => {
|
||||
) {
|
||||
return isLoadingWorkflow.value
|
||||
}
|
||||
return false
|
||||
return isInitialLoad.value
|
||||
})
|
||||
|
||||
const resultsWithKeys = computed(
|
||||
@@ -270,6 +271,27 @@ const selectedNodePack = computed<components['schemas']['Node'] | null>(() =>
|
||||
selectedNodePacks.value.length === 1 ? selectedNodePacks.value[0] : null
|
||||
)
|
||||
|
||||
const getLoadingCount = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
case ManagerTab.Installed:
|
||||
return comfyManagerStore.installedPacksIds?.size
|
||||
case ManagerTab.Workflow:
|
||||
return workflowPacks.value?.length
|
||||
case ManagerTab.Missing:
|
||||
return workflowPacks.value?.filter?.(
|
||||
(pack) => !comfyManagerStore.isPackInstalled(pack.id)
|
||||
)?.length
|
||||
default:
|
||||
return searchResults.value.length
|
||||
}
|
||||
}
|
||||
|
||||
const skeletonCardCount = computed(() => {
|
||||
const loadingCount = getLoadingCount()
|
||||
if (loadingCount) return loadingCount
|
||||
return isSmallScreen.value ? 12 : 16
|
||||
})
|
||||
|
||||
const selectNodePack = (
|
||||
nodePack: components['schemas']['Node'],
|
||||
event: MouseEvent
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div :style="gridStyle">
|
||||
<PackCardSkeleton v-for="n in skeletonCardCount" :key="n" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackCardSkeleton from '@/components/dialog/content/manager/skeleton/PackCardSkeleton.vue'
|
||||
|
||||
const { skeletonCardCount = 12, gridStyle } = defineProps<{
|
||||
skeletonCardCount?: number
|
||||
gridStyle: {
|
||||
display: string
|
||||
gridTemplateColumns: string
|
||||
padding: string
|
||||
gap: string
|
||||
}
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-lg border shadow-sm h-full overflow-hidden flex flex-col"
|
||||
data-virtual-grid-item
|
||||
>
|
||||
<!-- Card header - flush with top, approximately 15% of height -->
|
||||
<div class="w-full px-4 py-3 flex justify-between items-center border-b">
|
||||
<div class="flex items-center">
|
||||
<div class="w-6 h-6 flex items-center justify-center">
|
||||
<Skeleton shape="circle" width="1.5rem" height="1.5rem"></Skeleton>
|
||||
</div>
|
||||
<Skeleton width="5rem" height="1rem" class="ml-2"></Skeleton>
|
||||
</div>
|
||||
<Skeleton width="4rem" height="1.75rem" borderRadius="0.75rem"></Skeleton>
|
||||
</div>
|
||||
|
||||
<!-- Card content with icon on left and text on right -->
|
||||
<div class="flex-1 p-4 flex">
|
||||
<!-- Left icon - 64x64 -->
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<Skeleton width="4rem" height="4rem" borderRadius="0.5rem"></Skeleton>
|
||||
</div>
|
||||
|
||||
<!-- Right content -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Title -->
|
||||
<Skeleton width="80%" height="1rem" class="mb-2"></Skeleton>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<Skeleton width="100%" height="0.75rem" class="mb-1"></Skeleton>
|
||||
<Skeleton width="95%" height="0.75rem" class="mb-1"></Skeleton>
|
||||
<Skeleton width="90%" height="0.75rem"></Skeleton>
|
||||
</div>
|
||||
|
||||
<!-- Tags/Badges -->
|
||||
<div class="flex gap-2">
|
||||
<Skeleton
|
||||
width="4rem"
|
||||
height="1.5rem"
|
||||
borderRadius="0.75rem"
|
||||
></Skeleton>
|
||||
<Skeleton
|
||||
width="5rem"
|
||||
height="1.5rem"
|
||||
borderRadius="0.75rem"
|
||||
></Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card footer - similar to header -->
|
||||
<div class="w-full px-5 py-4 flex justify-between items-center border-t">
|
||||
<Skeleton width="4rem" height="0.8rem"></Skeleton>
|
||||
<Skeleton width="6rem" height="0.8rem"></Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import GridSkeleton from '../GridSkeleton.vue'
|
||||
import PackCardSkeleton from '../PackCardSkeleton.vue'
|
||||
|
||||
describe('GridSkeleton', () => {
|
||||
const mountComponent = ({
|
||||
props = {}
|
||||
}: Record<string, any> = {}): VueWrapper => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
return mount(GridSkeleton, {
|
||||
props: {
|
||||
gridStyle: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(19rem, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '1.5rem'
|
||||
},
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
stubs: {
|
||||
PackCardSkeleton: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders with default props', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('applies the provided grid style', () => {
|
||||
const customGridStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(15rem, 1fr))',
|
||||
padding: '1rem',
|
||||
gap: '1rem'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { gridStyle: customGridStyle }
|
||||
})
|
||||
|
||||
const gridElement = wrapper.element
|
||||
expect(gridElement.style.display).toBe('grid')
|
||||
expect(gridElement.style.gridTemplateColumns).toBe(
|
||||
'repeat(auto-fill, minmax(15rem, 1fr))'
|
||||
)
|
||||
expect(gridElement.style.padding).toBe('1rem')
|
||||
expect(gridElement.style.gap).toBe('1rem')
|
||||
})
|
||||
|
||||
it('renders the specified number of skeleton cards', async () => {
|
||||
const cardCount = 5
|
||||
const wrapper = mountComponent({
|
||||
props: { skeletonCardCount: cardCount }
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const skeletonCards = wrapper.findAllComponents(PackCardSkeleton)
|
||||
expect(skeletonCards.length).toBe(5)
|
||||
})
|
||||
})
|
||||
@@ -217,6 +217,22 @@ export default {
|
||||
plugins: [
|
||||
function ({ addVariant }) {
|
||||
addVariant('dark-theme', '.dark-theme &')
|
||||
},
|
||||
function ({ addUtilities }) {
|
||||
const newUtilities = {
|
||||
'.scrollbar-hide': {
|
||||
/* Firefox */
|
||||
'scrollbar-width': 'none',
|
||||
/* Webkit-based browsers */
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '1px'
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
'background-color': 'transparent'
|
||||
}
|
||||
}
|
||||
}
|
||||
addUtilities(newUtilities)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user