mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 13:59:54 +00:00
feature: model browser folder grouping (#7892)
This commit is contained in:
@@ -80,7 +80,9 @@
|
||||
>
|
||||
{{ contentTitle }}
|
||||
</h2>
|
||||
<div class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto">
|
||||
<div
|
||||
class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<div v-if="icon" class="py-0.5">
|
||||
<div v-if="icon" class="pt-0.5">
|
||||
<NavIcon :icon="icon" />
|
||||
</div>
|
||||
<i v-else class="text-neutral icon-[lucide--folder] text-xs shrink-0" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:key="badge.label"
|
||||
:class="
|
||||
cn(
|
||||
'px-2 py-1 rounded text-xs font-bold uppercase tracking-wider text-modal-card-tag-foreground bg-modal-card-tag-background'
|
||||
'px-2 py-1 rounded text-xs font-bold uppercase tracking-wider text-modal-card-tag-foreground bg-modal-card-tag-background break-all'
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
@update:model-value="handleFilterChange"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--arrow-up-down] size-3" />
|
||||
<i class="icon-[lucide--arrow-up-down]" />
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,48 @@ describe('useAssetBrowser', () => {
|
||||
|
||||
expect(result.description).toBe('loras model')
|
||||
})
|
||||
|
||||
it('removes category prefix from badge labels', () => {
|
||||
const apiAsset = createApiAsset({
|
||||
tags: ['models', 'checkpoint/stable-diffusion-v1-5']
|
||||
})
|
||||
|
||||
const { filteredAssets } = useAssetBrowser(ref([apiAsset]))
|
||||
const result = filteredAssets.value[0]
|
||||
|
||||
expect(result.badges).toContainEqual({
|
||||
label: 'stable-diffusion-v1-5',
|
||||
type: 'type'
|
||||
})
|
||||
})
|
||||
|
||||
it('handles tags without slash for badges', () => {
|
||||
const apiAsset = createApiAsset({
|
||||
tags: ['models', 'checkpoints']
|
||||
})
|
||||
|
||||
const { filteredAssets } = useAssetBrowser(ref([apiAsset]))
|
||||
const result = filteredAssets.value[0]
|
||||
|
||||
expect(result.badges).toContainEqual({
|
||||
label: 'checkpoints',
|
||||
type: 'type'
|
||||
})
|
||||
})
|
||||
|
||||
it('handles tags with multiple slashes in badges', () => {
|
||||
const apiAsset = createApiAsset({
|
||||
tags: ['models', 'checkpoint/subfolder/model-name']
|
||||
})
|
||||
|
||||
const { filteredAssets } = useAssetBrowser(ref([apiAsset]))
|
||||
const result = filteredAssets.value[0]
|
||||
|
||||
expect(result.badges).toContainEqual({
|
||||
label: 'subfolder/model-name',
|
||||
type: 'type'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tag-Based Filtering', () => {
|
||||
@@ -533,5 +575,58 @@ describe('useAssetBrowser', () => {
|
||||
selectedCategory.value = 'unknown'
|
||||
expect(contentTitle.value).toBe('Assets')
|
||||
})
|
||||
|
||||
it('groups models by top-level folder name', () => {
|
||||
const assets = [
|
||||
createApiAsset({
|
||||
id: 'asset-1',
|
||||
tags: ['models', 'Chatterbox/subfolder1/model1']
|
||||
}),
|
||||
createApiAsset({
|
||||
id: 'asset-2',
|
||||
tags: ['models', 'Chatterbox/subfolder2/model2']
|
||||
}),
|
||||
createApiAsset({
|
||||
id: 'asset-3',
|
||||
tags: ['models', 'Chatterbox/subfolder3/model3']
|
||||
}),
|
||||
createApiAsset({
|
||||
id: 'asset-4',
|
||||
tags: ['models', 'OtherFolder/subfolder1/model4']
|
||||
})
|
||||
]
|
||||
|
||||
const { availableCategories, selectedCategory, categoryFilteredAssets } =
|
||||
useAssetBrowser(ref(assets))
|
||||
|
||||
// Should group all Chatterbox subfolders under single category
|
||||
expect(availableCategories.value).toEqual([
|
||||
{ id: 'all', label: 'All Models', icon: 'icon-[lucide--folder]' },
|
||||
{
|
||||
id: 'Chatterbox',
|
||||
label: 'Chatterbox',
|
||||
icon: 'icon-[lucide--package]'
|
||||
},
|
||||
{
|
||||
id: 'OtherFolder',
|
||||
label: 'OtherFolder',
|
||||
icon: 'icon-[lucide--package]'
|
||||
}
|
||||
])
|
||||
|
||||
// When selecting Chatterbox category, should include all models from its subfolders
|
||||
selectedCategory.value = 'Chatterbox'
|
||||
expect(categoryFilteredAssets.value).toHaveLength(3)
|
||||
expect(categoryFilteredAssets.value.map((a) => a.id)).toEqual([
|
||||
'asset-1',
|
||||
'asset-2',
|
||||
'asset-3'
|
||||
])
|
||||
|
||||
// When selecting OtherFolder category, should include only its models
|
||||
selectedCategory.value = 'OtherFolder'
|
||||
expect(categoryFilteredAssets.value).toHaveLength(1)
|
||||
expect(categoryFilteredAssets.value[0].id).toBe('asset-4')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,18 @@ export type OwnershipOption = 'all' | 'my-models' | 'public-models'
|
||||
|
||||
function filterByCategory(category: string) {
|
||||
return (asset: AssetItem) => {
|
||||
return category === 'all' || asset.tags.includes(category)
|
||||
if (category === 'all') return true
|
||||
|
||||
// Check if any tag matches the category (for exact matches)
|
||||
if (asset.tags.includes(category)) return true
|
||||
|
||||
// Check if any tag's top-level folder matches the category
|
||||
return asset.tags.some((tag) => {
|
||||
if (typeof tag === 'string' && tag.includes('/')) {
|
||||
return tag.split('/')[0] === category
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +104,12 @@ export function useAssetBrowser(
|
||||
|
||||
// Type badge from non-root tag
|
||||
if (typeTag) {
|
||||
badges.push({ label: typeTag, type: 'type' })
|
||||
// Remove category prefix from badge label (e.g. "checkpoint/model" → "model")
|
||||
const badgeLabel = typeTag.includes('/')
|
||||
? typeTag.substring(typeTag.indexOf('/') + 1)
|
||||
: typeTag
|
||||
|
||||
badges.push({ label: badgeLabel, type: 'type' })
|
||||
}
|
||||
|
||||
// Base model badge from metadata
|
||||
@@ -125,6 +141,7 @@ export function useAssetBrowser(
|
||||
.filter((asset) => asset.tags[0] === 'models')
|
||||
.map((asset) => asset.tags[1])
|
||||
.filter((tag): tag is string => typeof tag === 'string' && tag.length > 0)
|
||||
.map((tag) => tag.split('/')[0]) // Extract top-level folder name
|
||||
|
||||
const uniqueCategories = Array.from(new Set(categories))
|
||||
.sort()
|
||||
|
||||
Reference in New Issue
Block a user