feat: add ownership filtering to FormDropdown

- Add ownership filter popover to FormDropdownMenuActions

- Extend FormDropdownItem with is_immutable field

- Filter assets by my-models/public-models in WidgetSelectDropdown

- Add OwnershipFilterOption type to filterTypes.ts

Amp-Thread-ID: https://ampcode.com/threads/T-019c10d8-2c5c-7763-a6cf-c033c6229eec
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-30 14:15:35 -08:00
parent c10a10c02d
commit 0d88869091
8 changed files with 195 additions and 17 deletions

View File

@@ -501,19 +501,28 @@ describe('useAssetBrowser', () => {
it('filters by ownership via filter bar - my-models', async () => {
const assets = [
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
createApiAsset({
name: 'my-model.safetensors',
is_immutable: false,
tags: ['models', 'checkpoints']
}),
createApiAsset({
name: 'public-model.safetensors',
is_immutable: true
is_immutable: true,
tags: ['models', 'checkpoints']
}),
createApiAsset({
name: 'another-my-model.safetensors',
is_immutable: false
is_immutable: false,
tags: ['models', 'checkpoints']
})
]
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
const { selectedNavItem, updateFilters, filteredAssets } =
useAssetBrowser(ref(assets))
// Must select a specific category for ownership filter to apply
selectedNavItem.value = 'checkpoints'
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
@@ -530,19 +539,28 @@ describe('useAssetBrowser', () => {
it('filters by ownership via filter bar - public-models', async () => {
const assets = [
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
createApiAsset({
name: 'my-model.safetensors',
is_immutable: false,
tags: ['models', 'loras']
}),
createApiAsset({
name: 'public-model.safetensors',
is_immutable: true
is_immutable: true,
tags: ['models', 'loras']
}),
createApiAsset({
name: 'another-public-model.safetensors',
is_immutable: true
is_immutable: true,
tags: ['models', 'loras']
})
]
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
const { selectedNavItem, updateFilters, filteredAssets } =
useAssetBrowser(ref(assets))
// Must select a specific category for ownership filter to apply
selectedNavItem.value = 'loras'
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
@@ -559,16 +577,23 @@ describe('useAssetBrowser', () => {
it('nav imported selection overrides filter bar ownership', async () => {
const assets = [
createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
createApiAsset({
name: 'my-model.safetensors',
is_immutable: false,
tags: ['models', 'checkpoints']
}),
createApiAsset({
name: 'public-model.safetensors',
is_immutable: true
is_immutable: true,
tags: ['models', 'checkpoints']
})
]
const { selectedNavItem, updateFilters, filteredAssets } =
useAssetBrowser(ref(assets))
// Must select a specific category for ownership filter to apply
selectedNavItem.value = 'checkpoints'
// Set filter bar to public-models
updateFilters({
sortBy: 'name-asc',

View File

@@ -103,6 +103,7 @@ export function useAssetBrowser(
const selectedOwnership = computed<OwnershipOption>(() => {
if (selectedNavItem.value === 'imported') return 'my-models'
if (selectedNavItem.value === 'all') return 'all'
return filters.value.ownership
})

View File

@@ -25,6 +25,14 @@ export interface FilterOption {
*/
export type OwnershipOption = 'all' | 'my-models' | 'public-models'
/**
* Ownership filter option for dropdowns/selects
*/
export interface OwnershipFilterOption {
id: OwnershipOption
name: string
}
/**
* Sort options for asset lists
* - 'default': Preserve original order (no sorting)

View File

@@ -12,7 +12,8 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue'
import type {
FilterOption,
OptionId
OptionId,
OwnershipOption
} from '@/platform/assets/types/filterTypes'
import { AssetKindKey } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import type {
@@ -87,6 +88,17 @@ const filterOptions = computed<FilterOption[]>(() => {
]
})
const ownershipSelected = ref<OwnershipOption>('all')
const showOwnershipFilter = computed(() => props.isAssetMode)
const ownershipOptions = computed(() => [
{ id: 'all' as const, name: t('assetBrowser.ownershipAll') },
{ id: 'my-models' as const, name: t('assetBrowser.ownershipMyModels') },
{
id: 'public-models' as const,
name: t('assetBrowser.ownershipPublicModels')
}
])
const selectedSet = ref<Set<OptionId>>(new Set())
/**
@@ -209,16 +221,26 @@ const assetItems = computed<FormDropdownItem[]>(() => {
id: asset.id,
name: getAssetFilename(asset),
label: getAssetDisplayName(asset),
preview_url: asset.preview_url
preview_url: asset.preview_url,
is_immutable: asset.is_immutable
}))
})
/**
* Filters asset items by ownership selection.
*/
const ownershipFilteredAssetItems = computed<FormDropdownItem[]>(() => {
if (ownershipSelected.value === 'all') return assetItems.value
const isPublic = ownershipSelected.value === 'public-models'
return assetItems.value.filter((item) => item.is_immutable === isPublic)
})
const allItems = computed<FormDropdownItem[]>(() => {
if (props.isAssetMode && assetData) {
if (missingValueItem.value) {
return [missingValueItem.value, ...assetItems.value]
return [missingValueItem.value, ...ownershipFilteredAssetItems.value]
}
return assetItems.value
return ownershipFilteredAssetItems.value
}
return [
...(missingValueItem.value ? [missingValueItem.value] : []),
@@ -415,12 +437,15 @@ function getMediaUrl(
v-model:selected="selectedSet"
v-model:filter-selected="filterSelected"
v-model:layout-mode="layoutMode"
v-model:ownership-selected="ownershipSelected"
:items="dropdownItems"
:placeholder="mediaPlaceholder"
:multiple="false"
:uploadable="uploadable"
:accept="acceptTypes"
:filter-options="filterOptions"
:show-ownership-filter="showOwnershipFilter"
:ownership-options="ownershipOptions"
v-bind="combinedProps"
class="w-full"
@update:selected="updateSelectedItems"

View File

@@ -7,7 +7,9 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import type {
FilterOption,
OptionId
OptionId,
OwnershipFilterOption,
OwnershipOption
} from '@/platform/assets/types/filterTypes'
import FormDropdownInput from './FormDropdownInput.vue'
@@ -29,6 +31,8 @@ interface Props {
accept?: string
filterOptions?: FilterOption[]
sortOptions?: SortOption[]
showOwnershipFilter?: boolean
ownershipOptions?: OwnershipFilterOption[]
isSelected?: (
selected: Set<OptionId>,
item: FormDropdownItem,
@@ -64,6 +68,9 @@ const layoutMode = defineModel<LayoutMode>('layoutMode', {
})
const files = defineModel<File[]>('files', { default: [] })
const searchQuery = defineModel<string>('searchQuery', { default: '' })
const ownershipSelected = defineModel<OwnershipOption>('ownershipSelected', {
default: 'all'
})
const toastStore = useToastStore()
const popoverRef = ref<InstanceType<typeof Popover>>()
@@ -202,8 +209,11 @@ async function customSearcher(
v-model:layout-mode="layoutMode"
v-model:sort-selected="sortSelected"
v-model:search-query="searchQuery"
v-model:ownership-selected="ownershipSelected"
:filter-options="filterOptions"
:sort-options="sortOptions"
:show-ownership-filter="showOwnershipFilter"
:ownership-options="ownershipOptions"
:disabled="disabled"
:searcher="customSearcher"
:items="sortedItems"

View File

@@ -5,7 +5,9 @@ import { cn } from '@/utils/tailwindUtil'
import type {
FilterOption,
OptionId
OptionId,
OwnershipFilterOption,
OwnershipOption
} from '@/platform/assets/types/filterTypes'
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
@@ -23,6 +25,8 @@ interface Props {
onCleanup: (cleanupFn: () => void) => void
) => Promise<void>
updateKey?: MaybeRefOrGetter<unknown>
showOwnershipFilter?: boolean
ownershipOptions?: OwnershipFilterOption[]
}
defineProps<Props>()
@@ -35,6 +39,7 @@ const filterSelected = defineModel<OptionId>('filterSelected')
const layoutMode = defineModel<LayoutMode>('layoutMode')
const sortSelected = defineModel<OptionId>('sortSelected')
const searchQuery = defineModel<string>('searchQuery')
const ownershipSelected = defineModel<OwnershipOption>('ownershipSelected')
// Handle item selection
</script>
@@ -54,9 +59,12 @@ const searchQuery = defineModel<string>('searchQuery')
v-model:layout-mode="layoutMode"
v-model:sort-selected="sortSelected"
v-model:search-query="searchQuery"
v-model:ownership-selected="ownershipSelected"
:sort-options="sortOptions"
:searcher
:update-key="updateKey"
:show-ownership-filter="showOwnershipFilter"
:ownership-options="ownershipOptions"
/>
<!-- List -->
<div class="relative flex h-full mt-2 overflow-y-scroll">

View File

@@ -3,13 +3,20 @@ import type { MaybeRefOrGetter } from 'vue'
import Popover from 'primevue/popover'
import { ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import type { OptionId } from '@/platform/assets/types/filterTypes'
import type {
OptionId,
OwnershipFilterOption,
OwnershipOption
} from '@/platform/assets/types/filterTypes'
import { cn } from '@/utils/tailwindUtil'
import FormSearchInput from '../FormSearchInput.vue'
import type { LayoutMode, SortOption } from './types'
const { t } = useI18n()
defineProps<{
searcher?: (
query: string,
@@ -17,11 +24,16 @@ defineProps<{
) => Promise<void>
sortOptions: SortOption[]
updateKey?: MaybeRefOrGetter<unknown>
showOwnershipFilter?: boolean
ownershipOptions?: OwnershipFilterOption[]
}>()
const layoutMode = defineModel<LayoutMode>('layoutMode')
const searchQuery = defineModel<string>('searchQuery')
const sortSelected = defineModel<OptionId>('sortSelected')
const ownershipSelected = defineModel<OwnershipOption>('ownershipSelected', {
default: 'all'
})
const actionButtonStyle = cn(
'h-8 bg-zinc-500/20 rounded-lg outline outline-1 outline-offset-[-1px] outline-node-component-border transition-all duration-150'
@@ -50,6 +62,25 @@ function handleSortSelected(item: SortOption) {
sortSelected.value = item.id
closeSortPopover()
}
const ownershipPopoverRef = useTemplateRef('ownershipPopoverRef')
const ownershipTriggerRef = useTemplateRef('ownershipTriggerRef')
const isOwnershipPopoverOpen = ref(false)
function toggleOwnershipPopover(event: Event) {
if (!ownershipPopoverRef.value || !ownershipTriggerRef.value) return
isOwnershipPopoverOpen.value = !isOwnershipPopoverOpen.value
ownershipPopoverRef.value.toggle(event, ownershipTriggerRef.value)
}
function closeOwnershipPopover() {
isOwnershipPopoverOpen.value = false
ownershipPopoverRef.value?.hide()
}
function handleOwnershipSelected(item: OwnershipFilterOption) {
ownershipSelected.value = item.id
closeOwnershipPopover()
}
</script>
<template>
@@ -132,6 +163,74 @@ function handleSortSelected(item: SortOption) {
</div>
</Popover>
<!-- Ownership Filter -->
<button
v-if="showOwnershipFilter && ownershipOptions?.length"
ref="ownershipTriggerRef"
:title="t('assetBrowser.ownership')"
:class="
cn(
resetInputStyle,
actionButtonStyle,
'relative w-8 flex justify-center items-center cursor-pointer',
'hover:outline-component-node-widget-background-highlighted',
'active:!scale-95'
)
"
@click="toggleOwnershipPopover"
>
<div
v-if="ownershipSelected !== 'all'"
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
/>
<i class="icon-[lucide--user] size-4" />
</button>
<!-- Ownership Popover -->
<Popover
ref="ownershipPopoverRef"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="{
root: {
class: 'absolute z-50'
},
content: {
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
}
}"
@hide="isOwnershipPopoverOpen = false"
>
<div
:class="
cn(
'flex flex-col gap-2 p-2 min-w-32',
'bg-component-node-background',
'rounded-lg outline outline-offset-[-1px] outline-component-node-border'
)
"
>
<button
v-for="item of ownershipOptions"
:key="item.id"
:class="
cn(
resetInputStyle,
'flex justify-between items-center h-6 cursor-pointer',
'hover:!text-blue-500'
)
"
@click="handleOwnershipSelected(item)"
>
<span>{{ item.name }}</span>
<i
v-if="ownershipSelected === item.id"
class="icon-[lucide--check] size-4"
/>
</button>
</div>
</Popover>
<!-- Layout Switch -->
<div
:class="

View File

@@ -15,6 +15,8 @@ export interface FormDropdownItem {
label?: string
/** Preview image/video URL */
preview_url?: string
/** Whether the item is immutable (public model) - used for ownership filtering */
is_immutable?: boolean
}
export interface SortOption<TId extends OptionId = OptionId> {