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:
Johnpaul Chiwetelu
2026-02-18 12:55:05 +01:00
committed by GitHub
parent 07e64a7f44
commit 2900e5e52e
3 changed files with 138 additions and 8 deletions

View File

@@ -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
})

View File

@@ -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({

View File

@@ -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)