mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 06:20:10 +00:00
fix: asset browser filters stick when navigating categories (#8945)
## Summary File format and base model filters in the asset browser persisted when navigating to categories that don't contain matching assets, showing empty results with no way to clear the filter. ## Changes - **What**: Apply the same scope-aware filtering pattern from the template selector dialog. Selected filters that don't exist in the current category become inactive (excluded from filtering) but are preserved so they reactivate when navigating back. Uses writable computeds in `AssetFilterBar` (matching `WorkflowTemplateSelectorDialog`) and active filter intersection in `useAssetBrowser` (matching `useTemplateFiltering`). ## Before https://github.com/user-attachments/assets/5c61e844-7ea0-489c-9c44-e0864dc916bc ## After https://github.com/user-attachments/assets/8372e174-107c-41e2-b8cf-b7ef59fe741b ## Review Focus The pattern mirrors `selectedModelObjects`/`selectedUseCaseObjects` in `WorkflowTemplateSelectorDialog.vue` and `activeModels`/`activeUseCases` in `useTemplateFiltering.ts`. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8945-fix-asset-browser-filters-stick-when-navigating-categories-30b6d73d365081609ac5c3982a1a03fc) by [Unito](https://www.unito.io)
This commit is contained in:
committed by
GitHub
parent
07e64a7f44
commit
2900e5e52e
@@ -9,7 +9,7 @@
|
||||
>
|
||||
<MultiSelect
|
||||
v-if="availableFileFormats.length > 0"
|
||||
v-model="fileFormats"
|
||||
v-model="activeFileFormatObjects"
|
||||
:label="$t('assetBrowser.fileFormats')"
|
||||
:options="availableFileFormats"
|
||||
class="min-w-32"
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<MultiSelect
|
||||
v-if="availableBaseModels.length > 0"
|
||||
v-model="baseModels"
|
||||
v-model="activeBaseModelObjects"
|
||||
:label="$t('assetBrowser.baseModels')"
|
||||
:options="availableBaseModels"
|
||||
class="min-w-32"
|
||||
@@ -83,22 +83,45 @@ const { assets = [], showOwnershipFilter = false } = defineProps<{
|
||||
showOwnershipFilter?: boolean
|
||||
}>()
|
||||
|
||||
const fileFormats = ref<SelectOption[]>([])
|
||||
const baseModels = ref<SelectOption[]>([])
|
||||
const selectedFileFormats = ref<SelectOption[]>([])
|
||||
const selectedBaseModels = ref<SelectOption[]>([])
|
||||
const sortBy = ref<AssetSortOption>('recent')
|
||||
const ownership = ref<OwnershipOption>('all')
|
||||
|
||||
const { availableFileFormats, availableBaseModels, ownershipOptions } =
|
||||
useAssetFilterOptions(() => assets)
|
||||
|
||||
// Only show selected items that exist in the current scope
|
||||
const activeFileFormatObjects = computed({
|
||||
get() {
|
||||
return selectedFileFormats.value.filter((opt) =>
|
||||
availableFileFormats.value.some((a) => a.value === opt.value)
|
||||
)
|
||||
},
|
||||
set(value: SelectOption[]) {
|
||||
selectedFileFormats.value = value
|
||||
}
|
||||
})
|
||||
|
||||
const activeBaseModelObjects = computed({
|
||||
get() {
|
||||
return selectedBaseModels.value.filter((opt) =>
|
||||
availableBaseModels.value.some((a) => a.value === opt.value)
|
||||
)
|
||||
},
|
||||
set(value: SelectOption[]) {
|
||||
selectedBaseModels.value = value
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
filterChange: [filters: AssetFilterState]
|
||||
}>()
|
||||
|
||||
function handleFilterChange() {
|
||||
emit('filterChange', {
|
||||
fileFormats: fileFormats.value.map((option: SelectOption) => option.value),
|
||||
baseModels: baseModels.value.map((option: SelectOption) => option.value),
|
||||
fileFormats: activeFileFormatObjects.value.map((opt) => opt.value),
|
||||
baseModels: activeBaseModelObjects.value.map((opt) => opt.value),
|
||||
sortBy: sortBy.value,
|
||||
ownership: ownership.value
|
||||
})
|
||||
|
||||
@@ -5,6 +5,12 @@ import { nextTick, ref } from 'vue'
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
@@ -736,6 +742,90 @@ describe('useAssetBrowser', () => {
|
||||
expect(contentTitle.value).toBe('Assets')
|
||||
})
|
||||
|
||||
it('ignores stale file format filter when navigating to category without that format', async () => {
|
||||
const assets = [
|
||||
createApiAsset({
|
||||
id: 'ckpt-safetensors',
|
||||
name: 'model.safetensors',
|
||||
tags: ['models', 'checkpoints']
|
||||
}),
|
||||
createApiAsset({
|
||||
id: 'lora-pt',
|
||||
name: 'lora.pt',
|
||||
tags: ['models', 'loras']
|
||||
}),
|
||||
createApiAsset({
|
||||
id: 'lora-pt-2',
|
||||
name: 'lora2.pt',
|
||||
tags: ['models', 'loras']
|
||||
})
|
||||
]
|
||||
|
||||
const { selectedNavItem, updateFilters, filteredAssets } =
|
||||
useAssetBrowser(ref(assets))
|
||||
|
||||
// Select safetensors filter while viewing checkpoints
|
||||
selectedNavItem.value = 'checkpoints'
|
||||
updateFilters({
|
||||
sortBy: 'recent',
|
||||
fileFormats: ['safetensors'],
|
||||
baseModels: [],
|
||||
ownership: 'all'
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(1)
|
||||
expect(filteredAssets.value[0].id).toBe('ckpt-safetensors')
|
||||
|
||||
// Navigate to loras category which has no .safetensors files
|
||||
selectedNavItem.value = 'loras'
|
||||
await nextTick()
|
||||
|
||||
// Should show all loras, not empty (stale filter should be ignored)
|
||||
expect(filteredAssets.value).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('ignores stale base model filter when navigating to category without that model', async () => {
|
||||
const assets = [
|
||||
createApiAsset({
|
||||
id: 'ckpt-sdxl',
|
||||
name: 'model.safetensors',
|
||||
tags: ['models', 'checkpoints'],
|
||||
user_metadata: { base_model: 'SDXL' }
|
||||
}),
|
||||
createApiAsset({
|
||||
id: 'lora-sd15',
|
||||
name: 'lora.pt',
|
||||
tags: ['models', 'loras'],
|
||||
user_metadata: { base_model: 'SD1.5' }
|
||||
})
|
||||
]
|
||||
|
||||
const { selectedNavItem, updateFilters, filteredAssets } =
|
||||
useAssetBrowser(ref(assets))
|
||||
|
||||
// Select SDXL base model filter while viewing checkpoints
|
||||
selectedNavItem.value = 'checkpoints'
|
||||
updateFilters({
|
||||
sortBy: 'recent',
|
||||
fileFormats: [],
|
||||
baseModels: ['SDXL'],
|
||||
ownership: 'all'
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(1)
|
||||
expect(filteredAssets.value[0].id).toBe('ckpt-sdxl')
|
||||
|
||||
// Navigate to loras which has no SDXL models
|
||||
selectedNavItem.value = 'loras'
|
||||
await nextTick()
|
||||
|
||||
// Should show all loras, not empty
|
||||
expect(filteredAssets.value).toHaveLength(1)
|
||||
expect(filteredAssets.value[0].id).toBe('lora-sd15')
|
||||
})
|
||||
|
||||
it('groups models by top-level folder name', () => {
|
||||
const assets = [
|
||||
createApiAsset({
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
OwnershipOption
|
||||
} from '@/platform/assets/types/filterTypes'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
|
||||
import {
|
||||
filterByBaseModels,
|
||||
filterByCategory,
|
||||
@@ -192,6 +193,22 @@ export function useAssetBrowser(
|
||||
return assets.value.filter(filterByCategory(selectedCategory.value))
|
||||
})
|
||||
|
||||
const { availableFileFormats, availableBaseModels } = useAssetFilterOptions(
|
||||
categoryFilteredAssets
|
||||
)
|
||||
|
||||
const activeFileFormats = computed(() =>
|
||||
filters.value.fileFormats.filter((f) =>
|
||||
availableFileFormats.value.some((opt) => opt.value === f)
|
||||
)
|
||||
)
|
||||
|
||||
const activeBaseModels = computed(() =>
|
||||
filters.value.baseModels.filter((m) =>
|
||||
availableBaseModels.value.some((opt) => opt.value === m)
|
||||
)
|
||||
)
|
||||
|
||||
const fuseOptions: UseFuseOptions<AssetItem> = {
|
||||
fuseOptions: {
|
||||
keys: [
|
||||
@@ -223,8 +240,8 @@ export function useAssetBrowser(
|
||||
|
||||
const filteredAssets = computed(() => {
|
||||
const filtered = searchFiltered.value
|
||||
.filter(filterByFileFormats(filters.value.fileFormats))
|
||||
.filter(filterByBaseModels(filters.value.baseModels))
|
||||
.filter(filterByFileFormats(activeFileFormats.value))
|
||||
.filter(filterByBaseModels(activeBaseModels.value))
|
||||
.filter(filterByOwnership(selectedOwnership.value))
|
||||
|
||||
const sortedAssets = sortAssets(filtered, filters.value.sortBy)
|
||||
|
||||
Reference in New Issue
Block a user