feat: add ownership and base model filtering, unify asset/dropdown types (#8497)

Add ownership and base model filtering to AssetBrowserModal and
FormDropdown widgets.

## Changes

- **Ownership filter**: Filter by All/My Models/Public Models (uses
`is_immutable` field)
- **Base model filter**: Multi-select filter with Clear Filters button
- **Type unification**: Replace `AssetDropdownItem` with
`FormDropdownItem`
- **Sorting unification**: Extract shared utilities to
`assetSortUtils.ts`
- **UI refactor**: Use `Button` component, Vue 3.5 prop shorthand, i18n
improvements

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-02-01 20:01:18 -08:00
committed by GitHub
parent 4e20b7522b
commit eaa3ff1579
30 changed files with 1200 additions and 368 deletions

View File

@@ -51,6 +51,7 @@
<template #contentFilter>
<AssetFilterBar
:assets="categoryFilteredAssets"
:show-ownership-filter
@filter-change="updateFilters"
@click.self="focusedAsset = null"
/>
@@ -125,19 +126,16 @@ const emit = defineEmits<{
provide(OnCloseKey, props.onClose ?? (() => {}))
// Compute the cache key based on nodeType or assetType
const cacheKey = computed(() => {
if (props.nodeType) return props.nodeType
if (props.assetType) return `tag:${props.assetType}`
return ''
})
// Read directly from store cache - reactive to any store updates
const fetchedAssets = computed(() => assetStore.getAssets(cacheKey.value))
const isStoreLoading = computed(() => assetStore.isModelLoading(cacheKey.value))
// Only show loading spinner when loading AND no cached data
const isLoading = computed(
() => isStoreLoading.value && fetchedAssets.value.length === 0
)
@@ -150,10 +148,8 @@ async function refreshAssets(): Promise<void> {
}
}
// Trigger background refresh on mount
void refreshAssets()
// Eagerly fetch model types so they're available when ModelInfoPanel loads
const { fetchModelTypes } = useModelTypes()
void fetchModelTypes()
@@ -210,6 +206,12 @@ const shouldShowLeftPanel = computed(() => {
return props.showLeftPanel ?? true
})
const showOwnershipFilter = computed(
() =>
!shouldShowLeftPanel.value ||
(selectedNavItem.value !== 'all' && selectedNavItem.value !== 'imported')
)
const emptyMessage = computed(() => {
if (!isImportedSelected.value) {
return isUploadButtonEnabled.value

View File

@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
import type { FilterState } from '@/platform/assets/components/AssetFilterBar.vue'
import type { AssetFilterState } from '@/platform/assets/types/filterTypes'
import {
createAssetWithSpecificBaseModel,
createAssetWithSpecificExtension,
@@ -142,15 +142,16 @@ describe('AssetFilterBar', () => {
expect(emitted!.length).toBeGreaterThanOrEqual(3)
// Check final state
const finalState: FilterState = emitted![
const finalState: AssetFilterState = emitted![
emitted!.length - 1
][0] as FilterState
][0] as AssetFilterState
expect(finalState.fileFormats).toEqual(['ckpt', 'safetensors'])
expect(finalState.baseModels).toEqual(['sdxl'])
expect(finalState.sortBy).toBe('name-desc')
expect(finalState.ownership).toBe('all')
})
it('ensures FilterState interface compliance', async () => {
it('ensures AssetFilterState interface compliance', async () => {
// Provide assets with options so filters are visible
const assets = [
createAssetWithSpecificExtension('safetensors'),
@@ -167,7 +168,7 @@ describe('AssetFilterBar', () => {
await nextTick()
const emitted = wrapper.emitted('filterChange')
const filterState = emitted![0][0] as FilterState
const filterState = emitted![0][0] as AssetFilterState
// Type and structure assertions
expect(Array.isArray(filterState.fileFormats)).toBe(true)

View File

@@ -26,6 +26,16 @@
data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange"
/>
<SingleSelect
v-if="showOwnershipFilter"
v-model="ownership"
:label="$t('assetBrowser.ownership')"
:options="ownershipOptions"
class="min-w-32"
data-component-id="asset-filter-ownership"
@update:model-value="handleFilterChange"
/>
</div>
<div class="flex items-center" data-component-id="asset-filter-bar-right">
@@ -54,44 +64,43 @@ import SingleSelect from '@/components/input/SingleSelect.vue'
import type { SelectOption } from '@/components/input/types'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type {
AssetFilterState,
AssetSortOption,
OwnershipOption
} from '@/platform/assets/types/filterTypes'
const { t } = useI18n()
type SortOption = 'recent' | 'name-asc' | 'name-desc'
const sortOptions = computed(() => [
{ name: t('assetBrowser.sortRecent'), value: 'recent' as const },
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' as const },
{ name: t('assetBrowser.sortZA'), value: 'name-desc' as const }
])
export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: SortOption
}
const { assets = [] } = defineProps<{
const { assets = [], showOwnershipFilter = false } = defineProps<{
assets?: AssetItem[]
showOwnershipFilter?: boolean
}>()
const fileFormats = ref<SelectOption[]>([])
const baseModels = ref<SelectOption[]>([])
const sortBy = ref<SortOption>('recent')
const sortBy = ref<AssetSortOption>('recent')
const ownership = ref<OwnershipOption>('all')
const { availableFileFormats, availableBaseModels } = useAssetFilterOptions(
() => assets
)
const { availableFileFormats, availableBaseModels, ownershipOptions } =
useAssetFilterOptions(() => assets)
const emit = defineEmits<{
filterChange: [filters: FilterState]
filterChange: [filters: AssetFilterState]
}>()
function handleFilterChange() {
emit('filterChange', {
fileFormats: fileFormats.value.map((option: SelectOption) => option.value),
baseModels: baseModels.value.map((option: SelectOption) => option.value),
sortBy: sortBy.value
sortBy: sortBy.value,
ownership: ownership.value
})
}
</script>