mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
[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:
committed by
GitHub
parent
2b9f7ecedf
commit
6850c45d63
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user