Skip to content

Commit 0168a98

Browse files
feat: add ownership filter to model browser
Add dropdown to filter models by ownership (All/My models/Public models). The filter only appears when user has uploaded models. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent bca7a43 commit 0168a98

File tree

6 files changed

+240
-8
lines changed

6 files changed

+240
-8
lines changed

src/locales/en/main.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,10 @@
21362136
"sortZA": "Z-A",
21372137
"sortRecent": "Recent",
21382138
"sortPopular": "Popular",
2139+
"ownership": "Ownership",
2140+
"ownershipAll": "All",
2141+
"ownershipMyModels": "My models",
2142+
"ownershipPublicModels": "Public models",
21392143
"selectFrameworks": "Select Frameworks",
21402144
"selectProjects": "Select Projects",
21412145
"sortingType": "Sorting Type",

src/platform/assets/components/AssetFilterBar.vue

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
data-component-id="asset-filter-base-models"
2727
@update:model-value="handleFilterChange"
2828
/>
29+
30+
<SingleSelect
31+
v-if="hasMutableAssets"
32+
v-model="ownership"
33+
:label="$t('assetBrowser.ownership')"
34+
:options="ownershipOptions"
35+
class="min-w-42"
36+
data-component-id="asset-filter-ownership"
37+
@update:model-value="handleFilterChange"
38+
/>
2939
</div>
3040

3141
<div class="flex items-center" data-component-id="asset-filter-bar-right">
@@ -46,7 +56,7 @@
4656
</template>
4757

4858
<script setup lang="ts">
49-
import { ref } from 'vue'
59+
import { computed, ref } from 'vue'
5060
5161
import MultiSelect from '@/components/input/MultiSelect.vue'
5262
import SingleSelect from '@/components/input/SingleSelect.vue'
@@ -59,6 +69,7 @@ export interface FilterState {
5969
fileFormats: string[]
6070
baseModels: string[]
6171
sortBy: string
72+
ownership: string
6273
}
6374
6475
const SORT_OPTIONS = [
@@ -71,17 +82,32 @@ type SortOption = (typeof SORT_OPTIONS)[number]['value']
7182
7283
const sortOptions = [...SORT_OPTIONS]
7384
85+
const OWNERSHIP_OPTIONS = [
86+
{ name: t('assetBrowser.ownershipAll'), value: 'all' },
87+
{ name: t('assetBrowser.ownershipMyModels'), value: 'my-models' },
88+
{ name: t('assetBrowser.ownershipPublicModels'), value: 'public-models' }
89+
] as const
90+
91+
type OwnershipOption = (typeof OWNERSHIP_OPTIONS)[number]['value']
92+
93+
const ownershipOptions = [...OWNERSHIP_OPTIONS]
94+
7495
const { assets = [] } = defineProps<{
7596
assets?: AssetItem[]
7697
}>()
7798
7899
const fileFormats = ref<SelectOption[]>([])
79100
const baseModels = ref<SelectOption[]>([])
80101
const sortBy = ref<SortOption>('recent')
102+
const ownership = ref<OwnershipOption>('all')
81103
82104
const { availableFileFormats, availableBaseModels } =
83105
useAssetFilterOptions(assets)
84106
107+
const hasMutableAssets = computed(() =>
108+
assets.some((asset) => !asset.is_immutable)
109+
)
110+
85111
const emit = defineEmits<{
86112
filterChange: [filters: FilterState]
87113
}>()
@@ -90,7 +116,8 @@ function handleFilterChange() {
90116
emit('filterChange', {
91117
fileFormats: fileFormats.value.map((option: SelectOption) => option.value),
92118
baseModels: baseModels.value.map((option: SelectOption) => option.value),
93-
sortBy: sortBy.value
119+
sortBy: sortBy.value,
120+
ownership: ownership.value
94121
})
95122
}
96123
</script>

src/platform/assets/composables/useAssetBrowser.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ function filterByBaseModels(models: string[]) {
3535
}
3636
}
3737

38+
function filterByOwnership(ownership: string) {
39+
return (asset: AssetItem) => {
40+
if (ownership === 'all') return true
41+
if (ownership === 'my-models') return !asset.is_immutable
42+
if (ownership === 'public-models') return asset.is_immutable
43+
return true
44+
}
45+
}
46+
3847
type AssetBadge = {
3948
label: string
4049
type: 'type' | 'base' | 'size'
@@ -65,7 +74,8 @@ export function useAssetBrowser(
6574
const filters = ref<FilterState>({
6675
sortBy: 'recent',
6776
fileFormats: [],
68-
baseModels: []
77+
baseModels: [],
78+
ownership: 'all'
6979
})
7080

7181
// Transform API asset to display asset
@@ -176,6 +186,7 @@ export function useAssetBrowser(
176186
const filtered = searchFiltered.value
177187
.filter(filterByFileFormats(filters.value.fileFormats))
178188
.filter(filterByBaseModels(filters.value.baseModels))
189+
.filter(filterByOwnership(filters.value.ownership))
179190

180191
const sortedAssets = [...filtered]
181192
sortedAssets.sort((a, b) => {

src/platform/assets/fixtures/ui-mock-assets.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,15 @@ export function createAssetWithoutUserMetadata() {
146146
return asset
147147
}
148148

149-
export function createAssetWithSpecificExtension(extension: string) {
149+
export function createAssetWithSpecificExtension(
150+
extension: string,
151+
isImmutable?: boolean
152+
) {
150153
const asset = createMockAssets(1)[0]
151154
asset.name = `test-model.${extension}`
155+
if (isImmutable !== undefined) {
156+
asset.is_immutable = isImmutable
157+
}
152158
return asset
153159
}
154160

tests-ui/platform/assets/components/AssetFilterBar.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,93 @@ describe('AssetFilterBar', () => {
247247
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
248248
expect(multiSelects).toHaveLength(0)
249249
})
250+
251+
it('hides ownership filter when no mutable assets', () => {
252+
const assets = [
253+
createAssetWithSpecificExtension('safetensors', true) // immutable
254+
]
255+
const wrapper = mountAssetFilterBar({ assets })
256+
257+
const ownershipSelects = wrapper
258+
.findAllComponents({ name: 'SingleSelect' })
259+
.filter(
260+
(component) => component.props('label') === 'assetBrowser.ownership'
261+
)
262+
263+
expect(ownershipSelects).toHaveLength(0)
264+
})
265+
266+
it('shows ownership filter when mutable assets exist', () => {
267+
const assets = [
268+
createAssetWithSpecificExtension('safetensors', false) // mutable
269+
]
270+
const wrapper = mountAssetFilterBar({ assets })
271+
272+
const ownershipSelects = wrapper
273+
.findAllComponents({ name: 'SingleSelect' })
274+
.filter(
275+
(component) => component.props('label') === 'assetBrowser.ownership'
276+
)
277+
278+
expect(ownershipSelects).toHaveLength(1)
279+
})
280+
281+
it('shows ownership filter when mixed assets exist', () => {
282+
const assets = [
283+
createAssetWithSpecificExtension('safetensors', true), // immutable
284+
createAssetWithSpecificExtension('ckpt', false) // mutable
285+
]
286+
const wrapper = mountAssetFilterBar({ assets })
287+
288+
const ownershipSelects = wrapper
289+
.findAllComponents({ name: 'SingleSelect' })
290+
.filter(
291+
(component) => component.props('label') === 'assetBrowser.ownership'
292+
)
293+
294+
expect(ownershipSelects).toHaveLength(1)
295+
})
296+
})
297+
298+
describe('Ownership Filter', () => {
299+
it('emits ownership filter changes', async () => {
300+
const assets = [
301+
createAssetWithSpecificExtension('safetensors', false) // mutable
302+
]
303+
const wrapper = mountAssetFilterBar({ assets })
304+
305+
const ownershipSelect = wrapper
306+
.findAllComponents({ name: 'SingleSelect' })
307+
.find(
308+
(component) => component.props('label') === 'assetBrowser.ownership'
309+
)
310+
311+
await ownershipSelect!.vm.$emit('update:modelValue', 'my-models')
312+
await nextTick()
313+
314+
const emitted = wrapper.emitted('filterChange')
315+
expect(emitted).toHaveLength(1)
316+
317+
const filterState = emitted![0][0] as FilterState
318+
expect(filterState.ownership).toBe('my-models')
319+
})
320+
321+
it('ownership filter defaults to "all"', async () => {
322+
const assets = [
323+
createAssetWithSpecificExtension('safetensors', false) // mutable
324+
]
325+
const wrapper = mountAssetFilterBar({ assets })
326+
327+
const sortSelect = wrapper
328+
.findAllComponents({ name: 'SingleSelect' })
329+
.find((component) => component.props('label') === 'assetBrowser.sortBy')
330+
331+
await sortSelect!.vm.$emit('update:modelValue', 'recent')
332+
await nextTick()
333+
334+
const emitted = wrapper.emitted('filterChange')
335+
const filterState = emitted![0][0] as FilterState
336+
expect(filterState.ownership).toBe('all')
337+
})
250338
})
251339
})

tests-ui/platform/assets/composables/useAssetBrowser.test.ts

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ describe('useAssetBrowser', () => {
249249
updateFilters({
250250
sortBy: 'name-asc',
251251
fileFormats: ['safetensors'],
252-
baseModels: []
252+
baseModels: [],
253+
ownership: 'all'
253254
})
254255
await nextTick()
255256

@@ -284,7 +285,8 @@ describe('useAssetBrowser', () => {
284285
updateFilters({
285286
sortBy: 'name-asc',
286287
fileFormats: [],
287-
baseModels: ['SDXL']
288+
baseModels: ['SDXL'],
289+
ownership: 'all'
288290
})
289291
await nextTick()
290292

@@ -335,7 +337,12 @@ describe('useAssetBrowser', () => {
335337

336338
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
337339

338-
updateFilters({ sortBy: 'name', fileFormats: [], baseModels: [] })
340+
updateFilters({
341+
sortBy: 'name',
342+
fileFormats: [],
343+
baseModels: [],
344+
ownership: 'all'
345+
})
339346
await nextTick()
340347

341348
const names = filteredAssets.value.map((asset) => asset.name)
@@ -355,7 +362,12 @@ describe('useAssetBrowser', () => {
355362

356363
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
357364

358-
updateFilters({ sortBy: 'recent', fileFormats: [], baseModels: [] })
365+
updateFilters({
366+
sortBy: 'recent',
367+
fileFormats: [],
368+
baseModels: [],
369+
ownership: 'all'
370+
})
359371
await nextTick()
360372

361373
const dates = filteredAssets.value.map((asset) => asset.created_at)
@@ -365,6 +377,90 @@ describe('useAssetBrowser', () => {
365377
'2024-01-01T00:00:00Z'
366378
])
367379
})
380+
381+
it('filters by ownership - all', async () => {
382+
const assets = [
383+
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
384+
createApiAsset({
385+
name: 'public-model.safetensors',
386+
is_immutable: true
387+
}),
388+
createApiAsset({
389+
name: 'another-my-model.safetensors',
390+
is_immutable: false
391+
})
392+
]
393+
394+
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
395+
396+
updateFilters({
397+
sortBy: 'name-asc',
398+
fileFormats: [],
399+
baseModels: [],
400+
ownership: 'all'
401+
})
402+
await nextTick()
403+
404+
expect(filteredAssets.value).toHaveLength(3)
405+
})
406+
407+
it('filters by ownership - my models only', async () => {
408+
const assets = [
409+
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
410+
createApiAsset({
411+
name: 'public-model.safetensors',
412+
is_immutable: true
413+
}),
414+
createApiAsset({
415+
name: 'another-my-model.safetensors',
416+
is_immutable: false
417+
})
418+
]
419+
420+
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
421+
422+
updateFilters({
423+
sortBy: 'name-asc',
424+
fileFormats: [],
425+
baseModels: [],
426+
ownership: 'my-models'
427+
})
428+
await nextTick()
429+
430+
expect(filteredAssets.value).toHaveLength(2)
431+
expect(filteredAssets.value.every((asset) => !asset.is_immutable)).toBe(
432+
true
433+
)
434+
})
435+
436+
it('filters by ownership - public models only', async () => {
437+
const assets = [
438+
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
439+
createApiAsset({
440+
name: 'public-model.safetensors',
441+
is_immutable: true
442+
}),
443+
createApiAsset({
444+
name: 'another-public-model.safetensors',
445+
is_immutable: true
446+
})
447+
]
448+
449+
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
450+
451+
updateFilters({
452+
sortBy: 'name-asc',
453+
fileFormats: [],
454+
baseModels: [],
455+
ownership: 'public-models'
456+
})
457+
await nextTick()
458+
459+
expect(filteredAssets.value).toHaveLength(2)
460+
expect(filteredAssets.value.every((asset) => asset.is_immutable)).toBe(
461+
true
462+
)
463+
})
368464
})
369465

370466
describe('Dynamic Category Extraction', () => {

0 commit comments

Comments
 (0)