[feat] Add ownership filter to model browser (#7201)

## Summary
Adds a dropdown filter to the model browser that allows users to filter
assets by ownership (All, My models, Public models), based on the
`is_immutable` property.

## Changes
- **Filter UI**: Added ownership dropdown in
[AssetFilterBar.vue](src/platform/assets/components/AssetFilterBar.vue#L30-L38)
that only appears when user has uploaded models
- **Filter Logic**: Implemented `filterByOwnership` function in
[useAssetBrowser.ts](src/platform/assets/composables/useAssetBrowser.ts#L38-L45)
to filter by `is_immutable` property
- **i18n**: Added translation strings for ownership filter options
- **Tests**: Added comprehensive tests for ownership filtering in both
composable and component test files

## Review Focus
- The ownership filter visibility logic correctly checks for mutable
assets (`!is_immutable`)
- Default filter value is 'all' to show all models initially
- Filter integrates cleanly with existing file format and base model
filters

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7201-feat-Add-ownership-filter-to-model-browser-2c16d73d365081f280f6d1e42e5400af)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Luke Mino-Altherr
2025-12-09 13:52:33 -08:00
committed by GitHub
parent 2b9f7ecedf
commit 6850c45d63
7 changed files with 328 additions and 74 deletions

View File

@@ -2160,15 +2160,19 @@
"noModelsInFolder": "No {type} available in this folder", "noModelsInFolder": "No {type} available in this folder",
"notSureLeaveAsIs": "Not sure? Just leave this as is", "notSureLeaveAsIs": "Not sure? Just leave this as is",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported", "onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
"ownership": "Ownership",
"ownershipAll": "All",
"ownershipMyModels": "My models",
"ownershipPublicModels": "Public models",
"selectFrameworks": "Select Frameworks", "selectFrameworks": "Select Frameworks",
"selectModelType": "Select model type", "selectModelType": "Select model type",
"selectProjects": "Select Projects", "selectProjects": "Select Projects",
"sortAZ": "A-Z", "sortAZ": "A-Z",
"sortBy": "Sort by", "sortBy": "Sort by",
"sortingType": "Sorting Type",
"sortPopular": "Popular", "sortPopular": "Popular",
"sortRecent": "Recent", "sortRecent": "Recent",
"sortZA": "Z-A", "sortZA": "Z-A",
"sortingType": "Sorting Type",
"tags": "Tags", "tags": "Tags",
"tagsHelp": "Separate tags with commas", "tagsHelp": "Separate tags with commas",
"tagsPlaceholder": "e.g., models, checkpoint", "tagsPlaceholder": "e.g., models, checkpoint",

View File

@@ -48,6 +48,7 @@
<template #contentFilter> <template #contentFilter>
<AssetFilterBar <AssetFilterBar
:assets="categoryFilteredAssets" :assets="categoryFilteredAssets"
:all-assets="fetchedAssets"
@filter-change="updateFilters" @filter-change="updateFilters"
/> />
</template> </template>

View File

@@ -26,6 +26,16 @@
data-component-id="asset-filter-base-models" data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange" @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>
<div class="flex items-center" data-component-id="asset-filter-bar-right"> <div class="flex items-center" data-component-id="asset-filter-bar-right">
@@ -46,21 +56,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, ref } from 'vue'
import MultiSelect from '@/components/input/MultiSelect.vue' import MultiSelect from '@/components/input/MultiSelect.vue'
import SingleSelect from '@/components/input/SingleSelect.vue' import SingleSelect from '@/components/input/SingleSelect.vue'
import type { SelectOption } from '@/components/input/types' import type { SelectOption } from '@/components/input/types'
import { t } from '@/i18n' import { t } from '@/i18n'
import type { OwnershipOption } from '@/platform/assets/composables/useAssetBrowser'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions' import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema' import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: string
}
const SORT_OPTIONS = [ const SORT_OPTIONS = [
{ name: t('assetBrowser.sortRecent'), value: 'recent' }, { name: t('assetBrowser.sortRecent'), value: 'recent' },
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' }, { name: t('assetBrowser.sortAZ'), value: 'name-asc' },
@@ -71,17 +76,37 @@ type SortOption = (typeof SORT_OPTIONS)[number]['value']
const sortOptions = [...SORT_OPTIONS] const sortOptions = [...SORT_OPTIONS]
const { assets = [] } = defineProps<{ const ownershipOptions = [
{ name: t('assetBrowser.ownershipAll'), value: 'all' },
{ name: t('assetBrowser.ownershipMyModels'), value: 'my-models' },
{ name: t('assetBrowser.ownershipPublicModels'), value: 'public-models' }
]
export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: string
ownership: OwnershipOption
}
const { assets = [], allAssets = [] } = defineProps<{
assets?: AssetItem[] assets?: AssetItem[]
allAssets?: AssetItem[]
}>() }>()
const fileFormats = ref<SelectOption[]>([]) const fileFormats = ref<SelectOption[]>([])
const baseModels = ref<SelectOption[]>([]) const baseModels = ref<SelectOption[]>([])
const sortBy = ref<SortOption>('recent') const sortBy = ref<SortOption>('recent')
const ownership = ref<OwnershipOption>('all')
const { availableFileFormats, availableBaseModels } = const { availableFileFormats, availableBaseModels } =
useAssetFilterOptions(assets) useAssetFilterOptions(assets)
const hasMutableAssets = computed(() => {
const assetsToCheck = allAssets.length ? allAssets : assets
return assetsToCheck.some((asset) => asset.is_immutable === false)
})
const emit = defineEmits<{ const emit = defineEmits<{
filterChange: [filters: FilterState] filterChange: [filters: FilterState]
}>() }>()
@@ -90,7 +115,8 @@ function handleFilterChange() {
emit('filterChange', { emit('filterChange', {
fileFormats: fileFormats.value.map((option: SelectOption) => option.value), fileFormats: fileFormats.value.map((option: SelectOption) => option.value),
baseModels: baseModels.value.map((option: SelectOption) => option.value), baseModels: baseModels.value.map((option: SelectOption) => option.value),
sortBy: sortBy.value sortBy: sortBy.value,
ownership: ownership.value
}) })
} }
</script> </script>

View File

@@ -11,6 +11,8 @@ import {
getAssetDescription getAssetDescription
} from '@/platform/assets/utils/assetMetadataUtils' } from '@/platform/assets/utils/assetMetadataUtils'
export type OwnershipOption = 'all' | 'my-models' | 'public-models'
function filterByCategory(category: string) { function filterByCategory(category: string) {
return (asset: AssetItem) => { return (asset: AssetItem) => {
return category === 'all' || asset.tags.includes(category) return category === 'all' || asset.tags.includes(category)
@@ -35,6 +37,15 @@ function filterByBaseModels(models: string[]) {
} }
} }
function filterByOwnership(ownership: OwnershipOption) {
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 = { type AssetBadge = {
label: string label: string
type: 'type' | 'base' | 'size' type: 'type' | 'base' | 'size'
@@ -65,7 +76,8 @@ export function useAssetBrowser(
const filters = ref<FilterState>({ const filters = ref<FilterState>({
sortBy: 'recent', sortBy: 'recent',
fileFormats: [], fileFormats: [],
baseModels: [] baseModels: [],
ownership: 'all'
}) })
// Transform API asset to display asset // Transform API asset to display asset
@@ -176,6 +188,7 @@ export function useAssetBrowser(
const filtered = searchFiltered.value const filtered = searchFiltered.value
.filter(filterByFileFormats(filters.value.fileFormats)) .filter(filterByFileFormats(filters.value.fileFormats))
.filter(filterByBaseModels(filters.value.baseModels)) .filter(filterByBaseModels(filters.value.baseModels))
.filter(filterByOwnership(filters.value.ownership))
const sortedAssets = [...filtered] const sortedAssets = [...filtered]
sortedAssets.sort((a, b) => { sortedAssets.sort((a, b) => {

View File

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

View File

@@ -73,51 +73,83 @@ function mountAssetFilterBar(props = {}) {
}) })
} }
// Helper functions to find filters by user-facing attributes
function findFileFormatsFilter(
wrapper: ReturnType<typeof mountAssetFilterBar>
) {
return wrapper.findComponent(
'[data-component-id="asset-filter-file-formats"]'
)
}
function findBaseModelsFilter(wrapper: ReturnType<typeof mountAssetFilterBar>) {
return wrapper.findComponent('[data-component-id="asset-filter-base-models"]')
}
function findOwnershipFilter(wrapper: ReturnType<typeof mountAssetFilterBar>) {
return wrapper.findComponent('[data-component-id="asset-filter-ownership"]')
}
function findSortFilter(wrapper: ReturnType<typeof mountAssetFilterBar>) {
return wrapper.findComponent('[data-component-id="asset-filter-sort"]')
}
describe('AssetFilterBar', () => { describe('AssetFilterBar', () => {
describe('Filter State Management', () => { describe('Filter State Management', () => {
it('handles multiple simultaneous filter changes correctly', async () => { it('handles multiple simultaneous filter changes correctly', async () => {
// Provide assets with options so filters are visible // Provide assets with options so filters are visible
const assets = [ const assets = [
createAssetWithSpecificExtension('safetensors'), createAssetWithSpecificExtension('safetensors'),
createAssetWithSpecificBaseModel('sd15') createAssetWithSpecificExtension('ckpt'),
createAssetWithSpecificBaseModel('sd15'),
createAssetWithSpecificBaseModel('sdxl')
] ]
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
// Update file formats // Update file formats
const fileFormatSelect = wrapper.findAllComponents({ const fileFormatSelect = findFileFormatsFilter(wrapper)
name: 'MultiSelect' const fileFormatSelectElement = fileFormatSelect.find('select')
})[0] const options = fileFormatSelectElement.findAll('option')
await fileFormatSelect.vm.$emit('update:modelValue', [ const ckptOption = options.find((o) => o.element.value === 'ckpt')!
{ name: '.ckpt', value: 'ckpt' }, const safetensorsOption = options.find(
{ name: '.safetensors', value: 'safetensors' } (o) => o.element.value === 'safetensors'
]) )!
ckptOption.element.selected = true
safetensorsOption.element.selected = true
await fileFormatSelectElement.trigger('change')
await nextTick() await nextTick()
// Update base models // Update base models
const baseModelSelect = wrapper.findAllComponents({ const baseModelSelect = findBaseModelsFilter(wrapper)
name: 'MultiSelect' const baseModelSelectElement = baseModelSelect.find('select')
})[1] const sdxlOption = baseModelSelectElement
await baseModelSelect.vm.$emit('update:modelValue', [ .findAll('option')
{ name: 'SD XL', value: 'sdxl' } .find((o) => o.element.value === 'sdxl')
]) sdxlOption!.element.selected = true
await baseModelSelectElement.trigger('change')
await nextTick() await nextTick()
// Update sort // Update sort
const sortSelect = wrapper.findComponent({ name: 'SingleSelect' }) const sortSelect = findSortFilter(wrapper)
await sortSelect.vm.$emit('update:modelValue', 'popular') const sortSelectElement = sortSelect.find('select')
sortSelectElement.element.value = 'name-desc'
await sortSelectElement.trigger('change')
await nextTick() await nextTick()
const emitted = wrapper.emitted('filterChange') const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(3) expect(emitted).toBeTruthy()
expect(emitted!.length).toBeGreaterThanOrEqual(3)
// Check final state // Check final state
const finalState: FilterState = emitted![2][0] as FilterState const finalState: FilterState = emitted![
emitted!.length - 1
][0] as FilterState
expect(finalState.fileFormats).toEqual(['ckpt', 'safetensors']) expect(finalState.fileFormats).toEqual(['ckpt', 'safetensors'])
expect(finalState.baseModels).toEqual(['sdxl']) expect(finalState.baseModels).toEqual(['sdxl'])
expect(finalState.sortBy).toBe('popular') expect(finalState.sortBy).toBe('name-desc')
}) })
it('ensures FilterState interface compliance', async () => { it('ensures FilterState interface compliance', async () => {
@@ -128,12 +160,11 @@ describe('AssetFilterBar', () => {
] ]
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
const fileFormatSelect = wrapper.findAllComponents({ const fileFormatSelect = findFileFormatsFilter(wrapper)
name: 'MultiSelect' const fileFormatSelectElement = fileFormatSelect.find('select')
})[0] const ckptOption = fileFormatSelectElement.findAll('option')[0]
await fileFormatSelect.vm.$emit('update:modelValue', [ ckptOption.element.selected = true
{ name: '.ckpt', value: 'ckpt' } await fileFormatSelectElement.trigger('change')
])
await nextTick() await nextTick()
@@ -165,10 +196,11 @@ describe('AssetFilterBar', () => {
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
const fileFormatSelect = wrapper.findAllComponents({ const fileFormatSelect = findFileFormatsFilter(wrapper)
name: 'MultiSelect' const options = fileFormatSelect.findAll('option')
})[0] expect(
expect(fileFormatSelect.props('options')).toEqual([ options.map((o) => ({ name: o.text(), value: o.element.value }))
).toEqual([
{ name: '.ckpt', value: 'ckpt' }, { name: '.ckpt', value: 'ckpt' },
{ name: '.pt', value: 'pt' }, { name: '.pt', value: 'pt' },
{ name: '.safetensors', value: 'safetensors' } { name: '.safetensors', value: 'safetensors' }
@@ -184,10 +216,11 @@ describe('AssetFilterBar', () => {
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
const baseModelSelect = wrapper.findAllComponents({ const baseModelSelect = findBaseModelsFilter(wrapper)
name: 'MultiSelect' const options = baseModelSelect.findAll('option')
})[1] expect(
expect(baseModelSelect.props('options')).toEqual([ options.map((o) => ({ name: o.text(), value: o.element.value }))
).toEqual([
{ name: 'sd15', value: 'sd15' }, { name: 'sd15', value: 'sd15' },
{ name: 'sd35', value: 'sd35' }, { name: 'sd35', value: 'sd35' },
{ name: 'sdxl', value: 'sdxl' } { name: 'sdxl', value: 'sdxl' }
@@ -200,26 +233,16 @@ describe('AssetFilterBar', () => {
const assets: AssetItem[] = [] // No assets = no file format options const assets: AssetItem[] = [] // No assets = no file format options
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
const fileFormatSelects = wrapper const fileFormatSelect = findFileFormatsFilter(wrapper)
.findAllComponents({ name: 'MultiSelect' }) expect(fileFormatSelect.exists()).toBe(false)
.filter(
(component) => component.props('label') === 'assetBrowser.fileFormats'
)
expect(fileFormatSelects).toHaveLength(0)
}) })
it('hides base model filter when no options available', () => { it('hides base model filter when no options available', () => {
const assets = [createAssetWithoutBaseModel()] // Asset without base model = no base model options const assets = [createAssetWithoutBaseModel()] // Asset without base model = no base model options
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
const baseModelSelects = wrapper const baseModelSelect = findBaseModelsFilter(wrapper)
.findAllComponents({ name: 'MultiSelect' }) expect(baseModelSelect.exists()).toBe(false)
.filter(
(component) => component.props('label') === 'assetBrowser.baseModels'
)
expect(baseModelSelects).toHaveLength(0)
}) })
it('shows both filters when options are available', () => { it('shows both filters when options are available', () => {
@@ -229,23 +252,106 @@ describe('AssetFilterBar', () => {
] ]
const wrapper = mountAssetFilterBar({ assets }) const wrapper = mountAssetFilterBar({ assets })
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' }) const fileFormatSelect = findFileFormatsFilter(wrapper)
const fileFormatSelect = multiSelects.find( const baseModelSelect = findBaseModelsFilter(wrapper)
(component) => component.props('label') === 'assetBrowser.fileFormats'
)
const baseModelSelect = multiSelects.find(
(component) => component.props('label') === 'assetBrowser.baseModels'
)
expect(fileFormatSelect).toBeDefined() expect(fileFormatSelect.exists()).toBe(true)
expect(baseModelSelect).toBeDefined() expect(baseModelSelect.exists()).toBe(true)
}) })
it('hides both filters when no assets provided', () => { it('hides both filters when no assets provided', () => {
const wrapper = mountAssetFilterBar() const wrapper = mountAssetFilterBar()
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' }) const fileFormatSelect = findFileFormatsFilter(wrapper)
expect(multiSelects).toHaveLength(0) const baseModelSelect = findBaseModelsFilter(wrapper)
expect(fileFormatSelect.exists()).toBe(false)
expect(baseModelSelect.exists()).toBe(false)
})
it('hides ownership filter when no mutable assets', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true) // immutable
]
const wrapper = mountAssetFilterBar({ assets })
const ownershipSelect = findOwnershipFilter(wrapper)
expect(ownershipSelect.exists()).toBe(false)
})
it('shows ownership filter when mutable assets exist', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })
const ownershipSelect = findOwnershipFilter(wrapper)
expect(ownershipSelect.exists()).toBe(true)
})
it('shows ownership filter when mixed assets exist', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true), // immutable
createAssetWithSpecificExtension('ckpt', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })
const ownershipSelect = findOwnershipFilter(wrapper)
expect(ownershipSelect.exists()).toBe(true)
})
it('shows ownership filter with allAssets when provided', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true) // immutable
]
const allAssets = [
createAssetWithSpecificExtension('safetensors', true), // immutable
createAssetWithSpecificExtension('ckpt', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets, allAssets })
const ownershipSelect = findOwnershipFilter(wrapper)
expect(ownershipSelect.exists()).toBe(true)
})
})
describe('Ownership Filter', () => {
it('emits ownership filter changes', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })
const ownershipSelect = findOwnershipFilter(wrapper)
expect(ownershipSelect.exists()).toBe(true)
const ownershipSelectElement = ownershipSelect.find('select')
ownershipSelectElement.element.value = 'my-models'
await ownershipSelectElement.trigger('change')
await nextTick()
const emitted = wrapper.emitted('filterChange')
expect(emitted).toBeTruthy()
const filterState = emitted![emitted!.length - 1][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 = findSortFilter(wrapper)
const sortSelectElement = sortSelect.find('select')
sortSelectElement.element.value = 'recent'
await sortSelectElement.trigger('change')
await nextTick()
const emitted = wrapper.emitted('filterChange')
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('all')
}) })
}) })
}) })

View File

@@ -249,7 +249,8 @@ describe('useAssetBrowser', () => {
updateFilters({ updateFilters({
sortBy: 'name-asc', sortBy: 'name-asc',
fileFormats: ['safetensors'], fileFormats: ['safetensors'],
baseModels: [] baseModels: [],
ownership: 'all'
}) })
await nextTick() await nextTick()
@@ -284,7 +285,8 @@ describe('useAssetBrowser', () => {
updateFilters({ updateFilters({
sortBy: 'name-asc', sortBy: 'name-asc',
fileFormats: [], fileFormats: [],
baseModels: ['SDXL'] baseModels: ['SDXL'],
ownership: 'all'
}) })
await nextTick() await nextTick()
@@ -335,7 +337,12 @@ describe('useAssetBrowser', () => {
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets)) const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
updateFilters({ sortBy: 'name', fileFormats: [], baseModels: [] }) updateFilters({
sortBy: 'name',
fileFormats: [],
baseModels: [],
ownership: 'all'
})
await nextTick() await nextTick()
const names = filteredAssets.value.map((asset) => asset.name) const names = filteredAssets.value.map((asset) => asset.name)
@@ -355,7 +362,12 @@ describe('useAssetBrowser', () => {
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets)) const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
updateFilters({ sortBy: 'recent', fileFormats: [], baseModels: [] }) updateFilters({
sortBy: 'recent',
fileFormats: [],
baseModels: [],
ownership: 'all'
})
await nextTick() await nextTick()
const dates = filteredAssets.value.map((asset) => asset.created_at) const dates = filteredAssets.value.map((asset) => asset.created_at)
@@ -367,6 +379,92 @@ describe('useAssetBrowser', () => {
}) })
}) })
describe('Ownership filtering', () => {
it('filters by ownership - all', async () => {
const assets = [
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
createApiAsset({
name: 'public-model.safetensors',
is_immutable: true
}),
createApiAsset({
name: 'another-my-model.safetensors',
is_immutable: false
})
]
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
baseModels: [],
ownership: 'all'
})
await nextTick()
expect(filteredAssets.value).toHaveLength(3)
})
it('filters by ownership - my models only', async () => {
const assets = [
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
createApiAsset({
name: 'public-model.safetensors',
is_immutable: true
}),
createApiAsset({
name: 'another-my-model.safetensors',
is_immutable: false
})
]
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
baseModels: [],
ownership: 'my-models'
})
await nextTick()
expect(filteredAssets.value).toHaveLength(2)
expect(filteredAssets.value.every((asset) => !asset.is_immutable)).toBe(
true
)
})
it('filters by ownership - public models only', async () => {
const assets = [
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
createApiAsset({
name: 'public-model.safetensors',
is_immutable: true
}),
createApiAsset({
name: 'another-public-model.safetensors',
is_immutable: true
})
]
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
baseModels: [],
ownership: 'public-models'
})
await nextTick()
expect(filteredAssets.value).toHaveLength(2)
expect(filteredAssets.value.every((asset) => asset.is_immutable)).toBe(
true
)
})
})
describe('Dynamic Category Extraction', () => { describe('Dynamic Category Extraction', () => {
it('extracts categories from asset tags', () => { it('extracts categories from asset tags', () => {
const assets = [ const assets = [