mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 16:24:06 +00:00
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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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="
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user