Skip to content
Open
4 changes: 4 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,10 @@
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular",
"ownership": "Ownership",
"ownershipAll": "All",
"ownershipMyModels": "My models",
"ownershipPublicModels": "Public models",
"selectFrameworks": "Select Frameworks",
"selectProjects": "Select Projects",
"sortingType": "Sorting Type",
Expand Down
1 change: 1 addition & 0 deletions src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<template #contentFilter>
<AssetFilterBar
:assets="categoryFilteredAssets"
:all-assets="fetchedAssets"
@filter-change="updateFilters"
/>
</template>
Expand Down
46 changes: 37 additions & 9 deletions src/platform/assets/components/AssetFilterBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange"
/>

<SingleSelect
v-if="hasMutableAssets"
v-model="ownership"
:label="$t('assetBrowser.ownership')"
:options="ownershipOptions"
class="min-w-42"
data-component-id="asset-filter-ownership"
@update:model-value="handleFilterChange"
/>
</div>

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

<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'

import MultiSelect from '@/components/input/MultiSelect.vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
Expand All @@ -55,12 +65,6 @@ import { t } from '@/i18n'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'

export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: string
}

const SORT_OPTIONS = [
{ name: t('assetBrowser.sortRecent'), value: 'recent' },
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
Expand All @@ -71,17 +75,40 @@ type SortOption = (typeof SORT_OPTIONS)[number]['value']

const sortOptions = [...SORT_OPTIONS]

const { assets = [] } = defineProps<{
const OWNERSHIP_OPTIONS = [
{ name: t('assetBrowser.ownershipAll'), value: 'all' },
{ name: t('assetBrowser.ownershipMyModels'), value: 'my-models' },
{ name: t('assetBrowser.ownershipPublicModels'), value: 'public-models' }
] as const

type OwnershipOption = (typeof OWNERSHIP_OPTIONS)[number]['value']

const ownershipOptions = [...OWNERSHIP_OPTIONS]

export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: string
ownership: OwnershipOption
}

const { assets = [], allAssets = [] } = defineProps<{
assets?: AssetItem[]
allAssets?: AssetItem[]
}>()

const fileFormats = ref<SelectOption[]>([])
const baseModels = ref<SelectOption[]>([])
const sortBy = ref<SortOption>('recent')
const ownership = ref<OwnershipOption>('all')

const { availableFileFormats, availableBaseModels } =
useAssetFilterOptions(assets)

const hasMutableAssets = computed(() =>
allAssets.some((asset) => asset.is_immutable === false)
)

const emit = defineEmits<{
filterChange: [filters: FilterState]
}>()
Expand All @@ -90,7 +117,8 @@ 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>
13 changes: 12 additions & 1 deletion src/platform/assets/composables/useAssetBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ function filterByBaseModels(models: string[]) {
}
}

function filterByOwnership(ownership: string) {
return (asset: AssetItem) => {
if (ownership === 'all') return true
if (ownership === 'my-models') return asset.is_immutable === false
if (ownership === 'public-models') return asset.is_immutable === true
return true
}
}

type AssetBadge = {
label: string
type: 'type' | 'base' | 'size'
Expand Down Expand Up @@ -65,7 +74,8 @@ export function useAssetBrowser(
const filters = ref<FilterState>({
sortBy: 'recent',
fileFormats: [],
baseModels: []
baseModels: [],
ownership: 'all'
})

// Transform API asset to display asset
Expand Down Expand Up @@ -176,6 +186,7 @@ export function useAssetBrowser(
const filtered = searchFiltered.value
.filter(filterByFileFormats(filters.value.fileFormats))
.filter(filterByBaseModels(filters.value.baseModels))
.filter(filterByOwnership(filters.value.ownership))

const sortedAssets = [...filtered]
sortedAssets.sort((a, b) => {
Expand Down
8 changes: 7 additions & 1 deletion src/platform/assets/fixtures/ui-mock-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,15 @@ export function createAssetWithoutUserMetadata() {
return asset
}

export function createAssetWithSpecificExtension(extension: string) {
export function createAssetWithSpecificExtension(
extension: string,
isImmutable?: boolean
) {
const asset = createMockAssets(1)[0]
asset.name = `test-model.${extension}`
if (isImmutable !== undefined) {
asset.is_immutable = isImmutable
}
return asset
}

Expand Down
97 changes: 95 additions & 2 deletions tests-ui/platform/assets/components/AssetFilterBar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ describe('AssetFilterBar', () => {
await nextTick()

// Update sort
const sortSelect = wrapper.findComponent({ name: 'SingleSelect' })
await sortSelect.vm.$emit('update:modelValue', 'popular')
const sortSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find((component) => component.props('label') === 'assetBrowser.sortBy')
expect(sortSelect).toBeTruthy()
await sortSelect!.vm.$emit('update:modelValue', 'popular')

await nextTick()

Expand Down Expand Up @@ -247,5 +250,95 @@ describe('AssetFilterBar', () => {
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
expect(multiSelects).toHaveLength(0)
})

it('hides ownership filter when no mutable assets', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true) // immutable
]
const wrapper = mountAssetFilterBar({ assets })

const ownershipSelects = wrapper
.findAllComponents({ name: 'SingleSelect' })
.filter(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelects).toHaveLength(0)
})

it('shows ownership filter when mutable assets exist', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets, allAssets: assets })

const ownershipSelects = wrapper
.findAllComponents({ name: 'SingleSelect' })
.filter(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelects).toHaveLength(1)
})

it('shows ownership filter when mixed assets exist', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true), // immutable
createAssetWithSpecificExtension('ckpt', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets, allAssets: assets })

const ownershipSelects = wrapper
.findAllComponents({ name: 'SingleSelect' })
.filter(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelects).toHaveLength(1)
})
Comment on lines +254 to +298
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Align allAssets usage in ownership visibility tests

The visibility tests correctly cover “no mutable”, “all mutable”, and “mixed” cases. For the “no mutable assets” case you only pass assets, while the other two pass both assets and allAssets. To better mirror real usage (where AssetBrowserModal always supplies allAssets) and make the intent explicit, consider also wiring allAssets there:

-      const wrapper = mountAssetFilterBar({ assets })
+      const wrapper = mountAssetFilterBar({ assets, allAssets: assets })

This keeps the test focused on “no mutable in the full asset set” rather than on the implicit defaulting behavior of allAssets.

🤖 Prompt for AI Agents
In tests-ui/platform/assets/components/AssetFilterBar.test.ts around lines 254
to 298, the first test ("hides ownership filter when no mutable assets") only
passes assets but not allAssets, making it rely on implicit defaulting; update
that test to pass allAssets: assets (i.e., mountAssetFilterBar({ assets,
allAssets: assets })) so the test explicitly mirrors real usage where
AssetBrowserModal supplies allAssets and verifies there are no mutable assets in
the full set.

})

describe('Ownership Filter', () => {
it('emits ownership filter changes', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets, allAssets: assets })

const ownershipSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelect).toBeTruthy()
await ownershipSelect!.vm.$emit('update:modelValue', 'my-models')
await nextTick()

const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(1)

const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('my-models')
})

it('ownership filter defaults to "all"', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })

const sortSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find((component) => component.props('label') === 'assetBrowser.sortBy')

expect(sortSelect).toBeTruthy()
await sortSelect!.vm.$emit('update:modelValue', 'recent')
await nextTick()

const emitted = wrapper.emitted('filterChange')
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('all')
})
Comment on lines +301 to +342
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Ownership behavior tests are good; consider small consistency tweaks

The new tests correctly assert that ownership changes propagate into filterChange and that ownership defaults to 'all' when only sort changes. Two optional polish points:

  • In the default test, also assert the emitted length for symmetry and clearer failures:
-      const emitted = wrapper.emitted('filterChange')
-      const filterState = emitted![0][0] as FilterState
+      const emitted = wrapper.emitted('filterChange')
+      expect(emitted).toHaveLength(1)
+      const filterState = emitted![0][0] as FilterState
  • Optionally pass allAssets: assets in the default test as well to match production wiring, though behavior here doesn’t currently depend on it.

These are minor and non‑blocking; behavior under test is covered well. Based on learnings, this keeps tests behavior‑focused rather than relying on incidental defaults.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('Ownership Filter', () => {
it('emits ownership filter changes', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets, allAssets: assets })
const ownershipSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find(
(component) => component.props('label') === 'assetBrowser.ownership'
)
expect(ownershipSelect).toBeTruthy()
await ownershipSelect!.vm.$emit('update:modelValue', 'my-models')
await nextTick()
const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(1)
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('my-models')
})
it('ownership filter defaults to "all"', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })
const sortSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find((component) => component.props('label') === 'assetBrowser.sortBy')
expect(sortSelect).toBeTruthy()
await sortSelect!.vm.$emit('update:modelValue', 'recent')
await nextTick()
const emitted = wrapper.emitted('filterChange')
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('all')
})
describe('Ownership Filter', () => {
it('emits ownership filter changes', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets, allAssets: assets })
const ownershipSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find(
(component) => component.props('label') === 'assetBrowser.ownership'
)
expect(ownershipSelect).toBeTruthy()
await ownershipSelect!.vm.$emit('update:modelValue', 'my-models')
await nextTick()
const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(1)
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('my-models')
})
it('ownership filter defaults to "all"', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })
const sortSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find((component) => component.props('label') === 'assetBrowser.sortBy')
expect(sortSelect).toBeTruthy()
await sortSelect!.vm.$emit('update:modelValue', 'recent')
await nextTick()
const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(1)
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('all')
})
🤖 Prompt for AI Agents
In tests-ui/platform/assets/components/AssetFilterBar.test.ts around lines 301
to 342, add a check in the "ownership filter defaults to 'all'" test to assert
the number of emitted 'filterChange' events (e.g.,
expect(emitted).toHaveLength(1)) for symmetry with the other test and clearer
failures, and optionally pass allAssets: assets into mountAssetFilterBar(...) to
mirror production wiring; update the test to capture emitted, assert its length,
then assert filterState.ownership === 'all'.

})
})
Loading