diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 6bfe081df2..7e01508f55 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -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", diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue index ef85ce5f9a..e593861774 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 a86a7a1a84..0a4fe0aa6a 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,7 +56,7 @@ diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts index e5913e94a9..083cf0e6e4 100644 --- a/src/platform/assets/composables/useAssetBrowser.ts +++ b/src/platform/assets/composables/useAssetBrowser.ts @@ -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' @@ -65,7 +74,8 @@ export function useAssetBrowser( const filters = ref({ sortBy: 'recent', fileFormats: [], - baseModels: [] + baseModels: [], + ownership: 'all' }) // Transform API asset to display asset @@ -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) => { diff --git a/src/platform/assets/fixtures/ui-mock-assets.ts b/src/platform/assets/fixtures/ui-mock-assets.ts index 482a48af93..a31c7b859c 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 d9762e9782..1f412d8096 100644 --- a/tests-ui/platform/assets/components/AssetFilterBar.test.ts +++ b/tests-ui/platform/assets/components/AssetFilterBar.test.ts @@ -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() @@ -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) + }) + }) + + 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') + }) }) }) diff --git a/tests-ui/platform/assets/composables/useAssetBrowser.test.ts b/tests-ui/platform/assets/composables/useAssetBrowser.test.ts index 92ea78c33a..9c724f32b7 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 = [