From 2900e5e52e8efb6273c1d3e992ec7ba1f96f4737 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:55:05 +0100 Subject: [PATCH] fix: asset browser filters stick when navigating categories (#8945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- .../assets/components/AssetFilterBar.vue | 35 ++++++-- .../composables/useAssetBrowser.test.ts | 90 +++++++++++++++++++ .../assets/composables/useAssetBrowser.ts | 21 ++++- 3 files changed, 138 insertions(+), 8 deletions(-) diff --git a/src/platform/assets/components/AssetFilterBar.vue b/src/platform/assets/components/AssetFilterBar.vue index 7e83c0e8c7..712639824e 100644 --- a/src/platform/assets/components/AssetFilterBar.vue +++ b/src/platform/assets/components/AssetFilterBar.vue @@ -9,7 +9,7 @@ > () -const fileFormats = ref([]) -const baseModels = ref([]) +const selectedFileFormats = ref([]) +const selectedBaseModels = ref([]) const sortBy = ref('recent') const ownership = ref('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 }) diff --git a/src/platform/assets/composables/useAssetBrowser.test.ts b/src/platform/assets/composables/useAssetBrowser.test.ts index 5a067527b8..498000cc1f 100644 --- a/src/platform/assets/composables/useAssetBrowser.test.ts +++ b/src/platform/assets/composables/useAssetBrowser.test.ts @@ -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 = { @@ -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({ diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts index 773a53ffe9..448a89b434 100644 --- a/src/platform/assets/composables/useAssetBrowser.ts +++ b/src/platform/assets/composables/useAssetBrowser.ts @@ -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 = { 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)