diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 9f21a6d2f..34f963d23 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -2160,15 +2160,19 @@
"noModelsInFolder": "No {type} available in this folder",
"notSureLeaveAsIs": "Not sure? Just leave this as is",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
+ "ownership": "Ownership",
+ "ownershipAll": "All",
+ "ownershipMyModels": "My models",
+ "ownershipPublicModels": "Public models",
"selectFrameworks": "Select Frameworks",
"selectModelType": "Select model type",
"selectProjects": "Select Projects",
"sortAZ": "A-Z",
"sortBy": "Sort by",
- "sortingType": "Sorting Type",
"sortPopular": "Popular",
"sortRecent": "Recent",
"sortZA": "Z-A",
+ "sortingType": "Sorting Type",
"tags": "Tags",
"tagsHelp": "Separate tags with commas",
"tagsPlaceholder": "e.g., models, checkpoint",
diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue
index ef85ce5f9..e59386177 100644
--- a/src/platform/assets/components/AssetBrowserModal.vue
+++ b/src/platform/assets/components/AssetBrowserModal.vue
@@ -48,6 +48,7 @@
diff --git a/src/platform/assets/components/AssetFilterBar.vue b/src/platform/assets/components/AssetFilterBar.vue
index a86a7a1a8..0ffcba32d 100644
--- a/src/platform/assets/components/AssetFilterBar.vue
+++ b/src/platform/assets/components/AssetFilterBar.vue
@@ -26,6 +26,16 @@
data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange"
/>
+
+
@@ -46,21 +56,16 @@
diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts
index e5913e94a..799834614 100644
--- a/src/platform/assets/composables/useAssetBrowser.ts
+++ b/src/platform/assets/composables/useAssetBrowser.ts
@@ -11,6 +11,8 @@ import {
getAssetDescription
} from '@/platform/assets/utils/assetMetadataUtils'
+export type OwnershipOption = 'all' | 'my-models' | 'public-models'
+
function filterByCategory(category: string) {
return (asset: AssetItem) => {
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 = {
label: string
type: 'type' | 'base' | 'size'
@@ -65,7 +76,8 @@ export function useAssetBrowser(
const filters = ref({
sortBy: 'recent',
fileFormats: [],
- baseModels: []
+ baseModels: [],
+ ownership: 'all'
})
// Transform API asset to display asset
@@ -176,6 +188,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) => {
diff --git a/src/platform/assets/fixtures/ui-mock-assets.ts b/src/platform/assets/fixtures/ui-mock-assets.ts
index 482a48af9..a31c7b859 100644
--- a/src/platform/assets/fixtures/ui-mock-assets.ts
+++ b/src/platform/assets/fixtures/ui-mock-assets.ts
@@ -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
}
diff --git a/tests-ui/platform/assets/components/AssetFilterBar.test.ts b/tests-ui/platform/assets/components/AssetFilterBar.test.ts
index d9762e978..49d58b55b 100644
--- a/tests-ui/platform/assets/components/AssetFilterBar.test.ts
+++ b/tests-ui/platform/assets/components/AssetFilterBar.test.ts
@@ -73,51 +73,83 @@ function mountAssetFilterBar(props = {}) {
})
}
+// Helper functions to find filters by user-facing attributes
+function findFileFormatsFilter(
+ wrapper: ReturnType
+) {
+ return wrapper.findComponent(
+ '[data-component-id="asset-filter-file-formats"]'
+ )
+}
+
+function findBaseModelsFilter(wrapper: ReturnType) {
+ return wrapper.findComponent('[data-component-id="asset-filter-base-models"]')
+}
+
+function findOwnershipFilter(wrapper: ReturnType) {
+ return wrapper.findComponent('[data-component-id="asset-filter-ownership"]')
+}
+
+function findSortFilter(wrapper: ReturnType) {
+ return wrapper.findComponent('[data-component-id="asset-filter-sort"]')
+}
+
describe('AssetFilterBar', () => {
describe('Filter State Management', () => {
it('handles multiple simultaneous filter changes correctly', async () => {
// Provide assets with options so filters are visible
const assets = [
createAssetWithSpecificExtension('safetensors'),
- createAssetWithSpecificBaseModel('sd15')
+ createAssetWithSpecificExtension('ckpt'),
+ createAssetWithSpecificBaseModel('sd15'),
+ createAssetWithSpecificBaseModel('sdxl')
]
const wrapper = mountAssetFilterBar({ assets })
// Update file formats
- const fileFormatSelect = wrapper.findAllComponents({
- name: 'MultiSelect'
- })[0]
- await fileFormatSelect.vm.$emit('update:modelValue', [
- { name: '.ckpt', value: 'ckpt' },
- { name: '.safetensors', value: 'safetensors' }
- ])
+ const fileFormatSelect = findFileFormatsFilter(wrapper)
+ const fileFormatSelectElement = fileFormatSelect.find('select')
+ const options = fileFormatSelectElement.findAll('option')
+ const ckptOption = options.find((o) => o.element.value === 'ckpt')!
+ const safetensorsOption = options.find(
+ (o) => o.element.value === 'safetensors'
+ )!
+ ckptOption.element.selected = true
+ safetensorsOption.element.selected = true
+ await fileFormatSelectElement.trigger('change')
await nextTick()
// Update base models
- const baseModelSelect = wrapper.findAllComponents({
- name: 'MultiSelect'
- })[1]
- await baseModelSelect.vm.$emit('update:modelValue', [
- { name: 'SD XL', value: 'sdxl' }
- ])
+ const baseModelSelect = findBaseModelsFilter(wrapper)
+ const baseModelSelectElement = baseModelSelect.find('select')
+ const sdxlOption = baseModelSelectElement
+ .findAll('option')
+ .find((o) => o.element.value === 'sdxl')
+ sdxlOption!.element.selected = true
+ await baseModelSelectElement.trigger('change')
await nextTick()
// Update sort
- const sortSelect = wrapper.findComponent({ name: 'SingleSelect' })
- await sortSelect.vm.$emit('update:modelValue', 'popular')
+ const sortSelect = findSortFilter(wrapper)
+ const sortSelectElement = sortSelect.find('select')
+ sortSelectElement.element.value = 'name-desc'
+ await sortSelectElement.trigger('change')
await nextTick()
const emitted = wrapper.emitted('filterChange')
- expect(emitted).toHaveLength(3)
+ expect(emitted).toBeTruthy()
+ expect(emitted!.length).toBeGreaterThanOrEqual(3)
// 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.baseModels).toEqual(['sdxl'])
- expect(finalState.sortBy).toBe('popular')
+ expect(finalState.sortBy).toBe('name-desc')
})
it('ensures FilterState interface compliance', async () => {
@@ -128,12 +160,11 @@ describe('AssetFilterBar', () => {
]
const wrapper = mountAssetFilterBar({ assets })
- const fileFormatSelect = wrapper.findAllComponents({
- name: 'MultiSelect'
- })[0]
- await fileFormatSelect.vm.$emit('update:modelValue', [
- { name: '.ckpt', value: 'ckpt' }
- ])
+ const fileFormatSelect = findFileFormatsFilter(wrapper)
+ const fileFormatSelectElement = fileFormatSelect.find('select')
+ const ckptOption = fileFormatSelectElement.findAll('option')[0]
+ ckptOption.element.selected = true
+ await fileFormatSelectElement.trigger('change')
await nextTick()
@@ -165,10 +196,11 @@ describe('AssetFilterBar', () => {
const wrapper = mountAssetFilterBar({ assets })
- const fileFormatSelect = wrapper.findAllComponents({
- name: 'MultiSelect'
- })[0]
- expect(fileFormatSelect.props('options')).toEqual([
+ const fileFormatSelect = findFileFormatsFilter(wrapper)
+ const options = fileFormatSelect.findAll('option')
+ expect(
+ options.map((o) => ({ name: o.text(), value: o.element.value }))
+ ).toEqual([
{ name: '.ckpt', value: 'ckpt' },
{ name: '.pt', value: 'pt' },
{ name: '.safetensors', value: 'safetensors' }
@@ -184,10 +216,11 @@ describe('AssetFilterBar', () => {
const wrapper = mountAssetFilterBar({ assets })
- const baseModelSelect = wrapper.findAllComponents({
- name: 'MultiSelect'
- })[1]
- expect(baseModelSelect.props('options')).toEqual([
+ const baseModelSelect = findBaseModelsFilter(wrapper)
+ const options = baseModelSelect.findAll('option')
+ expect(
+ options.map((o) => ({ name: o.text(), value: o.element.value }))
+ ).toEqual([
{ name: 'sd15', value: 'sd15' },
{ name: 'sd35', value: 'sd35' },
{ name: 'sdxl', value: 'sdxl' }
@@ -200,26 +233,16 @@ describe('AssetFilterBar', () => {
const assets: AssetItem[] = [] // No assets = no file format options
const wrapper = mountAssetFilterBar({ assets })
- const fileFormatSelects = wrapper
- .findAllComponents({ name: 'MultiSelect' })
- .filter(
- (component) => component.props('label') === 'assetBrowser.fileFormats'
- )
-
- expect(fileFormatSelects).toHaveLength(0)
+ const fileFormatSelect = findFileFormatsFilter(wrapper)
+ expect(fileFormatSelect.exists()).toBe(false)
})
it('hides base model filter when no options available', () => {
const assets = [createAssetWithoutBaseModel()] // Asset without base model = no base model options
const wrapper = mountAssetFilterBar({ assets })
- const baseModelSelects = wrapper
- .findAllComponents({ name: 'MultiSelect' })
- .filter(
- (component) => component.props('label') === 'assetBrowser.baseModels'
- )
-
- expect(baseModelSelects).toHaveLength(0)
+ const baseModelSelect = findBaseModelsFilter(wrapper)
+ expect(baseModelSelect.exists()).toBe(false)
})
it('shows both filters when options are available', () => {
@@ -229,23 +252,106 @@ describe('AssetFilterBar', () => {
]
const wrapper = mountAssetFilterBar({ assets })
- const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
- const fileFormatSelect = multiSelects.find(
- (component) => component.props('label') === 'assetBrowser.fileFormats'
- )
- const baseModelSelect = multiSelects.find(
- (component) => component.props('label') === 'assetBrowser.baseModels'
- )
+ const fileFormatSelect = findFileFormatsFilter(wrapper)
+ const baseModelSelect = findBaseModelsFilter(wrapper)
- expect(fileFormatSelect).toBeDefined()
- expect(baseModelSelect).toBeDefined()
+ expect(fileFormatSelect.exists()).toBe(true)
+ expect(baseModelSelect.exists()).toBe(true)
})
it('hides both filters when no assets provided', () => {
const wrapper = mountAssetFilterBar()
- const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
- expect(multiSelects).toHaveLength(0)
+ const fileFormatSelect = findFileFormatsFilter(wrapper)
+ 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')
})
})
})
diff --git a/tests-ui/platform/assets/composables/useAssetBrowser.test.ts b/tests-ui/platform/assets/composables/useAssetBrowser.test.ts
index 92ea78c33..9c724f32b 100644
--- a/tests-ui/platform/assets/composables/useAssetBrowser.test.ts
+++ b/tests-ui/platform/assets/composables/useAssetBrowser.test.ts
@@ -249,7 +249,8 @@ describe('useAssetBrowser', () => {
updateFilters({
sortBy: 'name-asc',
fileFormats: ['safetensors'],
- baseModels: []
+ baseModels: [],
+ ownership: 'all'
})
await nextTick()
@@ -284,7 +285,8 @@ describe('useAssetBrowser', () => {
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
- baseModels: ['SDXL']
+ baseModels: ['SDXL'],
+ ownership: 'all'
})
await nextTick()
@@ -335,7 +337,12 @@ describe('useAssetBrowser', () => {
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
- updateFilters({ sortBy: 'name', fileFormats: [], baseModels: [] })
+ updateFilters({
+ sortBy: 'name',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'all'
+ })
await nextTick()
const names = filteredAssets.value.map((asset) => asset.name)
@@ -355,7 +362,12 @@ describe('useAssetBrowser', () => {
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
- updateFilters({ sortBy: 'recent', fileFormats: [], baseModels: [] })
+ updateFilters({
+ sortBy: 'recent',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'all'
+ })
await nextTick()
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', () => {
it('extracts categories from asset tags', () => {
const assets = [