Skip to content

Commit 8120ed9

Browse files
load assets browser before fetch completes and show loading state (#6189)
## Summary Moves the fetch and post-fetch logic associated with the asset browser into the component and shows a loading state while fetching. To test, use this branch: comfyanonymous/ComfyUI#10045 https://github.com/user-attachments/assets/718974d5-efc7-46a0-bcd6-e82596d4c389 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6189-load-assets-browser-before-fetch-completes-and-show-loading-state-2946d73d365081879d1bd05d86e8c036) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <[email protected]>
1 parent 89ff825 commit 8120ed9

File tree

9 files changed

+272
-283
lines changed

9 files changed

+272
-283
lines changed

src/platform/assets/components/AssetBrowserModal.vue

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@
4040
<template #content>
4141
<AssetGrid
4242
:assets="filteredAssets"
43+
:loading="isLoading"
4344
@asset-select="handleAssetSelectAndEmit"
4445
/>
4546
</template>
4647
</BaseModalLayout>
4748
</template>
4849

4950
<script setup lang="ts">
50-
import { computed, provide } from 'vue'
51+
import { useAsyncState } from '@vueuse/core'
52+
import { computed, provide, watch } from 'vue'
53+
import { useI18n } from 'vue-i18n'
5154

5255
import SearchBox from '@/components/input/SearchBox.vue'
5356
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
@@ -57,6 +60,9 @@ import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
5760
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
5861
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
5962
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
63+
import { assetService } from '@/platform/assets/services/assetService'
64+
import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel'
65+
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
6066
import { OnCloseKey } from '@/types/widgetTypes'
6167

6268
const props = defineProps<{
@@ -65,29 +71,86 @@ const props = defineProps<{
6571
onSelect?: (asset: AssetItem) => void
6672
onClose?: () => void
6773
showLeftPanel?: boolean
68-
assets?: AssetItem[]
6974
title?: string
75+
assetType?: string
7076
}>()
7177

78+
const { t } = useI18n()
79+
7280
const emit = defineEmits<{
7381
'asset-select': [asset: AssetDisplayItem]
7482
close: []
7583
}>()
7684

7785
provide(OnCloseKey, props.onClose ?? (() => {}))
7886

87+
const fetchAssets = async () => {
88+
if (props.nodeType) {
89+
return (await assetService.getAssetsForNodeType(props.nodeType)) ?? []
90+
}
91+
92+
if (props.assetType) {
93+
return (await assetService.getAssetsByTag(props.assetType)) ?? []
94+
}
95+
96+
return []
97+
}
98+
99+
const {
100+
state: fetchedAssets,
101+
isLoading,
102+
execute
103+
} = useAsyncState<AssetItem[]>(fetchAssets, [], { immediate: false })
104+
105+
watch(
106+
() => [props.nodeType, props.assetType],
107+
async () => {
108+
await execute()
109+
},
110+
{ immediate: true }
111+
)
112+
79113
const {
80114
searchQuery,
81115
selectedCategory,
82116
availableCategories,
83-
contentTitle,
84117
categoryFilteredAssets,
85118
filteredAssets,
86119
updateFilters
87-
} = useAssetBrowser(props.assets)
120+
} = useAssetBrowser(fetchedAssets)
121+
122+
const modelToNodeStore = useModelToNodeStore()
123+
124+
const primaryCategoryTag = computed(() => {
125+
const assets = fetchedAssets.value ?? []
126+
const tagFromAssets = assets
127+
.map((asset) => asset.tags?.find((tag) => tag !== 'models'))
128+
.find((tag): tag is string => typeof tag === 'string' && tag.length > 0)
129+
130+
if (tagFromAssets) return tagFromAssets
131+
132+
if (props.nodeType) {
133+
const mapped = modelToNodeStore.getCategoryForNodeType(props.nodeType)
134+
if (mapped) return mapped
135+
}
136+
137+
if (props.assetType) return props.assetType
138+
139+
return 'models'
140+
})
141+
142+
const activeCategoryTag = computed(() => {
143+
if (selectedCategory.value !== 'all') {
144+
return selectedCategory.value
145+
}
146+
return primaryCategoryTag.value
147+
})
88148

89149
const displayTitle = computed(() => {
90-
return props.title ?? contentTitle.value
150+
if (props.title) return props.title
151+
152+
const label = formatCategoryLabel(activeCategoryTag.value)
153+
return t('assetBrowser.allCategory', { category: label })
91154
})
92155

93156
const shouldShowLeftPanel = computed(() => {

src/platform/assets/components/AssetGrid.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@
3737
<!-- Loading state -->
3838
<div
3939
v-if="loading"
40-
class="col-span-full flex items-center justify-center py-16"
40+
class="col-span-full flex items-center justify-center py-20"
4141
>
4242
<i
4343
class="icon-[lucide--loader]"
4444
:class="
45-
cn('size-6 animate-spin', 'text-stone-300 dark-theme:text-stone-200')
45+
cn('size-12 animate-spin', 'text-stone-300 dark-theme:text-stone-200')
4646
"
4747
/>
4848
</div>

src/platform/assets/composables/useAssetBrowser.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { computed, ref } from 'vue'
2+
import type { Ref } from 'vue'
23

34
import { d, t } from '@/i18n'
45
import type { FilterState } from '@/platform/assets/components/AssetFilterBar.vue'
@@ -65,7 +66,10 @@ export interface AssetDisplayItem extends AssetItem {
6566
* Asset Browser composable
6667
* Manages search, filtering, asset transformation and selection logic
6768
*/
68-
export function useAssetBrowser(assets: AssetItem[] = []) {
69+
export function useAssetBrowser(
70+
assetsSource: Ref<AssetItem[] | undefined> = ref<AssetItem[] | undefined>([])
71+
) {
72+
const assets = computed<AssetItem[]>(() => assetsSource.value ?? [])
6973
// State
7074
const searchQuery = ref('')
7175
const selectedCategory = ref('all')
@@ -116,9 +120,10 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
116120
}
117121

118122
const availableCategories = computed(() => {
119-
const categories = assets
120-
.filter((asset) => asset.tags[0] === 'models' && asset.tags[1])
123+
const categories = assets.value
124+
.filter((asset) => asset.tags[0] === 'models')
121125
.map((asset) => asset.tags[1])
126+
.filter((tag): tag is string => typeof tag === 'string' && tag.length > 0)
122127

123128
const uniqueCategories = Array.from(new Set(categories))
124129
.sort()
@@ -152,7 +157,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
152157

153158
// Category-filtered assets for filter options (before search/format/base model filters)
154159
const categoryFilteredAssets = computed(() => {
155-
return assets.filter(filterByCategory(selectedCategory.value))
160+
return assets.value.filter(filterByCategory(selectedCategory.value))
156161
})
157162

158163
const filteredAssets = computed(() => {
@@ -161,8 +166,8 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
161166
.filter(filterByFileFormats(filters.value.fileFormats))
162167
.filter(filterByBaseModels(filters.value.baseModels))
163168

164-
// Sort assets
165-
filtered.sort((a, b) => {
169+
const sortedAssets = [...filtered]
170+
sortedAssets.sort((a, b) => {
166171
switch (filters.value.sortBy) {
167172
case 'name-desc':
168173
return b.name.localeCompare(a.name)
@@ -179,7 +184,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
179184
})
180185

181186
// Transform to display format
182-
return filtered.map(transformAssetForDisplay)
187+
return sortedAssets.map(transformAssetForDisplay)
183188
})
184189

185190
function updateFilters(newFilters: FilterState) {

src/platform/assets/composables/useAssetBrowserDialog.ts

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { t } from '@/i18n'
21
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
32
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
4-
import { assetService } from '@/platform/assets/services/assetService'
53
import { useDialogStore } from '@/stores/dialogStore'
64
import type { DialogComponentProps } from '@/stores/dialogStore'
75

@@ -51,42 +49,13 @@ export const useAssetBrowserDialog = () => {
5149
dialogStore.closeDialog({ key: dialogKey })
5250
}
5351

54-
const assets: AssetItem[] = await assetService
55-
.getAssetsForNodeType(props.nodeType)
56-
.catch((error) => {
57-
console.error(
58-
'Failed to fetch assets for node type:',
59-
props.nodeType,
60-
error
61-
)
62-
return []
63-
})
64-
65-
// Extract node type category from first asset's tags (e.g., "loras", "checkpoints")
66-
// Tags are ordered: ["models", "loras"] so take the second tag
67-
const nodeTypeCategory =
68-
assets[0]?.tags?.find((tag) => tag !== 'models') ?? 'models'
69-
70-
const acronyms = new Set(['VAE', 'CLIP', 'GLIGEN'])
71-
const categoryLabel = nodeTypeCategory
72-
.split('_')
73-
.map((word) => {
74-
const uc = word.toUpperCase()
75-
return acronyms.has(uc) ? uc : word
76-
})
77-
.join(' ')
78-
79-
const title = t('assetBrowser.allCategory', { category: categoryLabel })
80-
8152
dialogStore.showDialog({
8253
key: dialogKey,
8354
component: AssetBrowserModal,
8455
props: {
8556
nodeType: props.nodeType,
8657
inputName: props.inputName,
8758
currentValue: props.currentValue,
88-
assets,
89-
title,
9059
onSelect: handleAssetSelected,
9160
onClose: () => dialogStore.closeDialog({ key: dialogKey })
9261
},
@@ -100,25 +69,12 @@ export const useAssetBrowserDialog = () => {
10069
dialogStore.closeDialog({ key: dialogKey })
10170
}
10271

103-
const assets = await assetService
104-
.getAssetsByTag(options.assetType)
105-
.catch((error) => {
106-
console.error(
107-
'Failed to fetch assets for tag:',
108-
options.assetType,
109-
error
110-
)
111-
return []
112-
})
113-
11472
dialogStore.showDialog({
11573
key: dialogKey,
11674
component: AssetBrowserModal,
11775
props: {
118-
nodeType: undefined,
119-
inputName: undefined,
120-
assets,
12176
showLeftPanel: true,
77+
assetType: options.assetType,
12278
title: options.title,
12379
onSelect: handleAssetSelected,
12480
onClose: () => dialogStore.closeDialog({ key: dialogKey })

src/platform/assets/schemas/assetSchema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { z } from 'zod'
44
const zAsset = z.object({
55
id: z.string(),
66
name: z.string(),
7-
asset_hash: z.string().optional(),
7+
asset_hash: z.string().nullish(),
88
size: z.number(),
9-
mime_type: z.string().optional(),
9+
mime_type: z.string().nullish(),
1010
tags: z.array(z.string()).optional().default([]),
1111
preview_id: z.string().nullable().optional(),
1212
preview_url: z.string().optional(),
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const ACRONYM_TAGS = new Set(['VAE', 'CLIP', 'GLIGEN'])
2+
3+
export function formatCategoryLabel(raw?: string): string {
4+
if (!raw) return 'Models'
5+
6+
return raw
7+
.split('_')
8+
.map((segment) => {
9+
const upper = segment.toUpperCase()
10+
if (ACRONYM_TAGS.has(upper)) return upper
11+
12+
const lower = segment.toLowerCase()
13+
return lower.charAt(0).toUpperCase() + lower.slice(1)
14+
})
15+
.join(' ')
16+
}

0 commit comments

Comments
 (0)