implement filters in search providers

This commit is contained in:
bymyself
2025-06-15 00:11:40 -07:00
parent 75077fe9ed
commit 3f036fcf51
9 changed files with 455 additions and 118 deletions

View File

@@ -30,9 +30,11 @@
v-model:searchQuery="searchQuery"
v-model:searchMode="searchMode"
v-model:sortField="sortField"
v-model:activeFilters="activeFilters"
:search-results="searchResults"
:suggestions="suggestions"
:sort-options="sortOptions"
:filter-options="filterOptions"
/>
<div class="flex-1 overflow-auto">
<div
@@ -121,6 +123,7 @@ import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
@@ -132,6 +135,7 @@ const { initialTab } = defineProps<{
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
const { getPackById } = useComfyRegistryStore()
const systemStatsStore = useSystemStatsStore()
const persistedState = useManagerStatePersistence()
const initialState = persistedState.loadStoredState()
@@ -181,7 +185,9 @@ const {
searchMode,
sortField,
suggestions,
sortOptions
sortOptions,
activeFilters,
filterOptions
} = useRegistrySearch({
initialSortField: initialState.sortField,
initialSearchMode: initialState.searchMode,
@@ -464,8 +470,13 @@ whenever(selectedNodePack, async () => {
})
let gridContainer: HTMLElement | null = null
onMounted(() => {
onMounted(async () => {
gridContainer = document.getElementById('results-grid')
// Fetch system stats if not already loaded
if (!systemStatsStore.systemStats && !systemStatsStore.isLoading) {
await systemStatsStore.fetchSystemStats()
}
})
watch(searchQuery, () => {
gridContainer ??= document.getElementById('results-grid')

View File

@@ -25,23 +25,78 @@
@option-select="onOptionSelect"
/>
</div>
<div class="flex mt-3 text-sm">
<div class="flex gap-6 ml-1">
<SearchFilterDropdown
v-model:modelValue="searchMode"
:options="filterOptions"
:label="$t('g.filter')"
/>
<SearchFilterDropdown
v-model:modelValue="sortField"
:options="availableSortOptions"
:label="$t('g.sort')"
/>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between mt-3 text-sm">
<div class="flex gap-6 ml-1">
<SearchFilterDropdown
v-model:modelValue="searchMode"
:options="searchModeOptions"
:label="$t('g.filter')"
/>
<SearchFilterDropdown
v-model:modelValue="sortField"
:options="availableSortOptions"
:label="$t('g.sort')"
/>
</div>
<div class="flex items-center gap-4 mr-6">
<small v-if="hasResults" class="text-color-secondary">
{{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }}
</small>
</div>
</div>
<div class="flex items-center gap-4 ml-6">
<small v-if="hasResults" class="text-color-secondary">
{{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }}
</small>
<!-- Add search refinement dropdowns if provider supports them -->
<div v-if="filterOptions?.length" class="flex gap-3 ml-1 text-sm">
<template v-for="filterOption in filterOptions" :key="filterOption.id">
<div class="flex items-center gap-1">
<span class="text-muted">{{ filterOption.label }}:</span>
<Dropdown
v-if="filterOption.type === 'single-select'"
:model-value="selectedFilters[filterOption.id] as string"
:options="filterOption.options || []"
option-label="label"
option-value="value"
placeholder="Any"
:show-clear="true"
class="min-w-[6rem] border-none bg-transparent shadow-none"
:pt="{
input: { class: 'py-0 px-1 border-none' },
trigger: { class: 'hidden' },
panel: { class: 'shadow-md' },
item: { class: 'py-2 px-3 text-sm' }
}"
@update:model-value="
$event
? (selectedFilters[filterOption.id] = $event)
: delete selectedFilters[filterOption.id]
"
/>
<MultiSelect
v-else-if="filterOption.type === 'multi-select'"
:model-value="selectedFilters[filterOption.id] as string[]"
:options="filterOption.options || []"
option-label="label"
option-value="value"
display="chip"
class="min-w-[6rem] border-none bg-transparent shadow-none"
:pt="{
input: { class: 'py-0 px-1 border-none' },
trigger: { class: 'hidden' },
panel: { class: 'shadow-md' },
item: { class: 'py-2 px-3 text-sm' },
label: { class: 'py-0 px-1 text-sm' },
header: { class: 'p-2' },
filterInput: { class: 'text-sm' },
emptyMessage: { class: 'text-sm text-muted p-3' }
}"
@update:model-value="
$event?.length > 0
? (selectedFilters[filterOption.id] = $event)
: delete selectedFilters[filterOption.id]
"
/>
</div>
</template>
</div>
</div>
</div>
@@ -52,31 +107,36 @@ import { stubTrue } from 'lodash'
import AutoComplete, {
AutoCompleteOptionSelectEvent
} from 'primevue/autocomplete'
import Dropdown from 'primevue/dropdown'
import MultiSelect from 'primevue/multiselect'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import {
type SearchOption,
SortableAlgoliaField
} from '@/types/comfyManagerTypes'
import { type SearchOption } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import type {
ActiveFilters,
QuerySuggestion,
SearchFilter,
SearchMode,
SortableField
} from '@/types/searchServiceTypes'
const { searchResults, sortOptions } = defineProps<{
const { searchResults, sortOptions, filterOptions } = defineProps<{
searchResults?: components['schemas']['Node'][]
suggestions?: QuerySuggestion[]
sortOptions?: SortableField[]
filterOptions?: SearchFilter[]
}>()
const searchQuery = defineModel<string>('searchQuery')
const searchMode = defineModel<SearchMode>('searchMode', { default: 'packs' })
const sortField = defineModel<string>('sortField', {
default: SortableAlgoliaField.Downloads
default: 'total_install'
})
const selectedFilters = defineModel<ActiveFilters>('activeFilters', {
default: () => ({})
})
const { t } = useI18n()
@@ -92,7 +152,7 @@ const availableSortOptions = computed<SearchOption<string>[]>(() => {
label: field.label
}))
})
const filterOptions: SearchOption<SearchMode>[] = [
const searchModeOptions: SearchOption<SearchMode>[] = [
{ id: 'packs', label: t('manager.filter.nodePack') },
{ id: 'nodes', label: t('g.nodes') }
]

View File

@@ -1,18 +1,20 @@
import { watchDebounced } from '@vueuse/core'
import { orderBy } from 'lodash'
import { computed, ref, watch } from 'vue'
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'
import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
import type { SearchAttribute } from '@/types/algoliaTypes'
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { QuerySuggestion, SearchMode } from '@/types/searchServiceTypes'
import type {
ActiveFilters,
QuerySuggestion,
SearchMode
} from '@/types/searchServiceTypes'
type RegistryNodePack = components['schemas']['Node']
const SEARCH_DEBOUNCE_TIME = 320
const DEFAULT_SORT_FIELD = SortableAlgoliaField.Downloads // Set in the index configuration
const DEFAULT_SORT_FIELD = 'total_install' // Downloads field in the database
/**
* Composable for managing UI state of Comfy Node Registry search.
@@ -40,6 +42,7 @@ export function useRegistrySearch(
const searchQuery = ref(initialSearchQuery)
const searchResults = ref<RegistryNodePack[]>([])
const suggestions = ref<QuerySuggestion[]>([])
const activeFilters = ref<ActiveFilters>({})
const searchAttributes = computed<SearchAttribute[]>(() =>
searchMode.value === 'nodes' ? ['comfy_nodes'] : ['name', 'description']
@@ -47,43 +50,40 @@ export function useRegistrySearch(
const searchGateway = useRegistrySearchGateway()
const { searchPacks, clearSearchCache, getSortValue, getSortableFields } =
searchGateway
const {
searchPacks,
clearSearchCache,
getSortableFields,
getFilterableFields
} = searchGateway
const updateSearchResults = async (options: { append?: boolean }) => {
isLoading.value = true
if (!options.append) {
pageNumber.value = 0
}
// Get the sort direction from the provider's sortable fields
const sortableFields = getSortableFields()
const fieldConfig = sortableFields.find((f) => f.id === sortField.value)
const sortDirection = fieldConfig?.direction || 'desc'
const { nodePacks, querySuggestions } = await searchPacks(
searchQuery.value,
{
pageSize: pageSize.value,
pageNumber: pageNumber.value,
restrictSearchableAttributes: searchAttributes.value
restrictSearchableAttributes: searchAttributes.value,
filters: activeFilters.value,
sortField: sortField.value,
sortDirection
}
)
let sortedPacks = nodePacks
// Results are sorted by the default field to begin with -- so don't manually sort again
if (sortField.value && sortField.value !== DEFAULT_SORT_FIELD) {
// Get the sort direction from the provider's sortable fields
const sortableFields = getSortableFields()
const fieldConfig = sortableFields.find((f) => f.id === sortField.value)
const direction = fieldConfig?.direction || 'desc'
sortedPacks = orderBy(
nodePacks,
[(pack) => getSortValue(pack, sortField.value)],
[direction]
)
}
if (options.append && searchResults.value?.length) {
searchResults.value = searchResults.value.concat(sortedPacks)
searchResults.value = searchResults.value.concat(nodePacks)
} else {
searchResults.value = sortedPacks
searchResults.value = nodePacks
}
suggestions.value = querySuggestions
isLoading.value = false
@@ -93,6 +93,7 @@ export function useRegistrySearch(
const onPageChange = () => updateSearchResults({ append: true })
watch([sortField, searchMode], onQueryChange)
watch(activeFilters, onQueryChange, { deep: true })
watch(pageNumber, onPageChange)
watchDebounced(searchQuery, onQueryChange, {
debounce: SEARCH_DEBOUNCE_TIME,
@@ -103,6 +104,29 @@ export function useRegistrySearch(
return getSortableFields()
})
const filterOptions = computed(() => {
return getFilterableFields()
})
// Initialize filters with default values when they become available
const filterOptionsInitialized = ref(false)
watch(
filterOptions,
(newOptions) => {
if (!filterOptionsInitialized.value && newOptions.length > 0) {
const defaultFilters: ActiveFilters = {}
for (const option of newOptions) {
if (option.defaultValue !== undefined) {
defaultFilters[option.id] = option.defaultValue
}
}
activeFilters.value = { ...activeFilters.value, ...defaultFilters }
filterOptionsInitialized.value = true
}
},
{ immediate: true }
)
return {
isLoading,
pageNumber,
@@ -113,6 +137,8 @@ export function useRegistrySearch(
suggestions,
searchResults,
sortOptions,
activeFilters,
filterOptions,
clearCache: clearSearchCache
}
}

View File

@@ -1,3 +1,5 @@
import { ref } from 'vue'
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
import { useComfyRegistrySearchProvider } from '@/services/providers/registrySearchProvider'
import type { SearchNodePacksParams } from '@/types/algoliaTypes'
@@ -21,6 +23,11 @@ interface ProviderState {
const CIRCUIT_BREAKER_THRESHOLD = 3 // Number of failures before circuit opens
const CIRCUIT_BREAKER_TIMEOUT = 60000 // 1 minute before retry
// Global state shared across all uses of the gateway
const providers: ProviderState[] = []
const activeProviderIndex = ref(0)
let isInitialized = false
/**
* API Gateway for registry search providers with circuit breaker pattern.
* Acts as a single entry point that routes search requests to appropriate providers
@@ -32,27 +39,29 @@ const CIRCUIT_BREAKER_TIMEOUT = 60000 // 1 minute before retry
* - Automatic failover: Cascades through providers on failure
*/
export const useRegistrySearchGateway = (): NodePackSearchProvider => {
const providers: ProviderState[] = []
let activeProviderIndex = 0
// Initialize providers only once
if (!isInitialized) {
// Initialize providers in priority order
try {
providers.push({
provider: useAlgoliaSearchProvider(),
name: 'Algolia',
isHealthy: true,
consecutiveFailures: 0
})
} catch (error) {
console.warn('Failed to initialize Algolia provider:', error)
}
// Initialize providers in priority order
try {
providers.push({
provider: useAlgoliaSearchProvider(),
name: 'Algolia',
provider: useComfyRegistrySearchProvider(),
name: 'ComfyRegistry',
isHealthy: true,
consecutiveFailures: 0
})
} catch (error) {
console.warn('Failed to initialize Algolia provider:', error)
}
providers.push({
provider: useComfyRegistrySearchProvider(),
name: 'ComfyRegistry',
isHealthy: true,
consecutiveFailures: 0
})
isInitialized = true
}
// TODO: Add an "offline" provider that operates on a local cache of the registry.
@@ -109,7 +118,7 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
*/
const getActiveProvider = (): NodePackSearchProvider => {
// First, try to use the current active provider if it's healthy
const currentProvider = providers[activeProviderIndex]
const currentProvider = providers[activeProviderIndex.value]
if (currentProvider && isCircuitClosed(currentProvider)) {
return currentProvider.provider
}
@@ -118,7 +127,7 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
for (let i = 0; i < providers.length; i++) {
const providerState = providers[i]
if (isCircuitClosed(providerState)) {
activeProviderIndex = i
activeProviderIndex.value = i
return providerState.provider
}
}
@@ -131,8 +140,8 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
* Move to the next provider if available.
*/
const updateActiveProviderOnFailure = () => {
if (activeProviderIndex < providers.length - 1) {
activeProviderIndex++
if (activeProviderIndex.value < providers.length - 1) {
activeProviderIndex.value++
}
}
@@ -149,14 +158,14 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
for (let attempts = 0; attempts < providers.length; attempts++) {
try {
const provider = getActiveProvider()
const providerState = providers[activeProviderIndex]
const providerState = providers[activeProviderIndex.value]
const result = await provider.searchPacks(query, params)
recordSuccess(providerState)
return result
} catch (error) {
lastError = error as Error
const providerState = providers[activeProviderIndex]
const providerState = providers[activeProviderIndex.value]
recordFailure(providerState, lastError)
console.warn(
`${providerState.name} search provider failed (${providerState.consecutiveFailures} failures):`,
@@ -218,10 +227,22 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
return getActiveProvider().getSortableFields()
}
/**
* Get the filterable fields for the active provider.
* This is now a computed property that will react to provider changes.
*/
const getFilterableFields = () => {
// Access activeProviderIndex.value to establish reactivity
void activeProviderIndex.value
const provider = getActiveProvider()
return provider.getFilterableFields()
}
return {
searchPacks,
clearSearchCache,
getSortValue,
getSortableFields
getSortableFields,
getFilterableFields
}
}

View File

@@ -20,6 +20,7 @@ import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type {
NodePackSearchProvider,
SearchFilter,
SearchPacksResult,
SortableField
} from '@/types/searchServiceTypes'
@@ -105,7 +106,14 @@ export const useAlgoliaSearchProvider = (): NodePackSearchProvider => {
params: SearchNodePacksParams
): Promise<SearchPacksResult> => {
const { pageSize, pageNumber } = params
const rest = omit(params, ['pageSize', 'pageNumber'])
const rest = omit(params, [
'pageSize',
'pageNumber',
'sortField',
'sortDirection',
'filters'
])
// TODO:'filters', `sortField` and `sortDirection` need to be mapped to the appropriate Algolia syntax later
const requests: SearchQuery[] = [
{
@@ -223,10 +231,16 @@ export const useAlgoliaSearchProvider = (): NodePackSearchProvider => {
]
}
const getFilterableFields = (): SearchFilter[] => {
// Algolia provider doesn't support filters yet, returning empty array
return []
}
return {
searchPacks,
clearSearchCache,
getSortValue,
getSortableFields
getSortableFields,
getFilterableFields
}
}

View File

@@ -1,13 +1,17 @@
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { SearchNodePacksParams } from '@/types/algoliaTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { components, operations } from '@/types/comfyRegistryTypes'
import type {
NodePackSearchProvider,
SearchFilter,
SearchPacksResult,
SortableField
} from '@/types/searchServiceTypes'
type RegistryNodePack = components['schemas']['Node']
type ListNodesParams = operations['listAllNodes']['parameters']['query']
type SearchNodesParams = operations['searchNodes']['parameters']['query']
/**
* Search provider for the Comfy Registry.
@@ -15,6 +19,7 @@ type RegistryNodePack = components['schemas']['Node']
*/
export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
const registryStore = useComfyRegistryStore()
const systemStatsStore = useSystemStatsStore()
/**
* Search for node packs using the Comfy Registry API.
@@ -23,20 +28,72 @@ export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
query: string,
params: SearchNodePacksParams
): Promise<SearchPacksResult> => {
const { pageSize, pageNumber, restrictSearchableAttributes } = params
const {
pageSize,
pageNumber,
restrictSearchableAttributes,
filters,
sortField,
sortDirection
} = params
// Determine search mode based on searchable attributes
const isNodeSearch = restrictSearchableAttributes?.includes('comfy_nodes')
const hasSearchQuery = query && query.trim().length > 0
const searchParams = {
search: isNodeSearch ? undefined : query,
comfy_node_search: isNodeSearch ? query : undefined,
limit: pageSize,
offset: pageNumber * pageSize
let searchResult: { nodes?: RegistryNodePack[] } | null = null
if (hasSearchQuery) {
// Use /nodes/search endpoint when there's a search query
const searchParams: SearchNodesParams = {
search: isNodeSearch ? undefined : query,
comfy_node_search: isNodeSearch ? query : undefined,
limit: pageSize,
page: pageNumber + 1 // API uses 1-based page numbers
}
// Apply filters that are supported by search endpoint
if (filters) {
if (typeof filters.supported_os === 'string') {
searchParams.supported_os = filters.supported_os
}
// Map from our unified filter name to the search endpoint's parameter name
if (typeof filters.supported_accelerator === 'string') {
searchParams.supported_accelerator = filters.supported_accelerator
}
}
searchResult = await registryStore.search.call(searchParams)
} else {
// Use /nodes endpoint when there's no search query (supports more parameters)
const listParams: ListNodesParams = {
limit: pageSize,
page: pageNumber + 1 // API uses 1-based page numbers
}
// Apply filters that are supported by list endpoint
if (filters) {
if (typeof filters.supported_os === 'string') {
listParams.supported_os = filters.supported_os
}
if (typeof filters.supported_accelerator === 'string') {
listParams.supported_accelerator = filters.supported_accelerator
}
if (typeof filters.timestamp === 'string') {
listParams.timestamp = filters.timestamp
}
}
// Apply sort if provided (only supported by list endpoint)
if (sortField) {
const sortParam =
sortDirection === 'desc' ? `${sortField};desc` : sortField
listParams.sort = [sortParam]
}
searchResult = await registryStore.listAllPacks.call(listParams)
}
const searchResult = await registryStore.search.call(searchParams)
if (!searchResult || !searchResult.nodes) {
return {
nodePacks: [],
@@ -59,13 +116,13 @@ export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
sortField: string
): string | number => {
switch (sortField) {
case 'downloads':
case 'total_install':
return pack.downloads ?? 0
case 'name':
return pack.name ?? ''
case 'publisher':
case 'publisher_name':
return pack.publisher?.name ?? ''
case 'updated':
case 'last_updated':
return pack.latest_version?.createdAt
? new Date(pack.latest_version.createdAt).getTime()
: 0
@@ -76,10 +133,111 @@ export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
const getSortableFields = (): SortableField[] => {
return [
{ id: 'downloads', label: 'Downloads', direction: 'desc' },
{ id: 'total_install', label: 'Downloads', direction: 'desc' },
{ id: 'name', label: 'Name', direction: 'asc' },
{ id: 'publisher', label: 'Publisher', direction: 'asc' },
{ id: 'updated', label: 'Updated', direction: 'desc' }
{ id: 'publisher_name', label: 'Publisher', direction: 'asc' },
{ id: 'last_updated', label: 'Updated', direction: 'desc' }
]
}
/**
* Map system OS to filter value
*/
const getDefaultOSFilter = (): string | undefined => {
const stats = systemStatsStore.systemStats
if (!stats?.system?.os) return undefined
const osLower = stats.system.os.toLowerCase()
if (osLower.includes('windows')) return 'windows'
if (osLower.includes('darwin') || osLower.includes('mac')) return 'macos'
if (osLower.includes('linux')) return 'linux'
return undefined
}
/**
* Map system GPU to filter value
*/
const getDefaultAcceleratorFilter = (): string | undefined => {
const stats = systemStatsStore.systemStats
if (!stats?.devices || stats.devices.length === 0) return undefined
// Look for the first GPU device
for (const device of stats.devices) {
const deviceType = device.type.toLowerCase()
if (deviceType.includes('cuda')) return 'cuda'
if (deviceType.includes('mps')) return 'mps'
if (deviceType.includes('rocm')) return 'rocm'
if (deviceType.includes('directml')) return 'directml'
}
return undefined
}
const getFilterableFields = (): SearchFilter[] => {
return [
{
id: 'supported_os',
label: 'Operating System',
type: 'single-select',
options: [
{ value: 'windows', label: 'Windows' },
{ value: 'macos', label: 'macOS' },
{ value: 'linux', label: 'Linux' }
],
defaultValue: getDefaultOSFilter()
},
{
// Note: search endpoint uses singular, list endpoint uses plural
id: 'supported_accelerator',
label: 'GPU Support',
type: 'single-select',
options: [
{ value: 'cuda', label: 'CUDA (NVIDIA)' },
{ value: 'directml', label: 'DirectML' },
{ value: 'rocm', label: 'ROCm (AMD)' },
{ value: 'mps', label: 'Metal (Apple)' }
],
defaultValue: getDefaultAcceleratorFilter()
},
{
// Note: timestamp filter is only available on the list endpoint (no search query)
id: 'timestamp',
label: 'Updated Since',
type: 'single-select',
options: [
{
value: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
label: 'Last 24 hours'
},
{
value: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
label: 'Last week'
},
{
value: new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000
).toISOString(),
label: 'Last month'
},
{
value: new Date(
Date.now() - 90 * 24 * 60 * 60 * 1000
).toISOString(),
label: 'Last 3 months'
},
{
value: new Date(
Date.now() - 180 * 24 * 60 * 60 * 1000
).toISOString(),
label: 'Last 6 months'
},
{
value: new Date(
Date.now() - 365 * 24 * 60 * 60 * 1000
).toISOString(),
label: 'Last year'
}
]
}
]
}
@@ -87,6 +245,7 @@ export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
searchPacks,
clearSearchCache,
getSortValue,
getSortableFields
getSortableFields,
getFilterableFields
}
}

View File

@@ -1,7 +1,4 @@
import type {
BaseSearchParamsWithoutQuery,
Hit
} from 'algoliasearch/dist/lite/browser'
import type { Hit } from 'algoliasearch/dist/lite/browser'
import type { components } from '@/types/comfyRegistryTypes'
@@ -86,8 +83,11 @@ export interface NodesIndexSuggestion {
/**
* Parameters for searching the Algolia index.
*/
export type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
export interface SearchNodePacksParams {
pageSize: number
pageNumber: number
restrictSearchableAttributes?: SearchAttribute[]
filters?: Record<string, string | string[] | boolean>
sortField?: string
sortDirection?: 'asc' | 'desc'
}

View File

@@ -7,11 +7,28 @@ type RegistryNodePack = components['schemas']['Node']
* Search mode for filtering results
*/
export type SearchMode = 'nodes' | 'packs'
export type QuerySuggestion = {
query: string
popularity: number
}
export interface SearchFilter {
id: string
label: string
type: 'multi-select' | 'single-select' | 'boolean'
options?: FilterOption[]
defaultValue?: string | string[] | boolean
}
export interface FilterOption {
value: string
label: string
icon?: string
}
export type ActiveFilters = Record<string, string | string[] | boolean>
export interface SearchPacksResult {
nodePacks: RegistryNodePack[]
querySuggestions: QuerySuggestion[]
@@ -46,4 +63,10 @@ export interface NodePackSearchProvider {
* Get the list of sortable fields supported by this provider
*/
getSortableFields(): SortableField[]
/**
* Get the list of filterable fields supported by this provider
* Providers that don't support filters should return an empty array
*/
getFilterableFields(): SearchFilter[]
}

View File

@@ -30,14 +30,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -63,7 +65,8 @@ describe('useRegistrySearchGateway', () => {
.mockResolvedValue({ nodePacks: [], querySuggestions: [] }),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
@@ -101,14 +104,16 @@ describe('useRegistrySearchGateway', () => {
.mockRejectedValueOnce(new Error('Algolia failed')),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn().mockResolvedValue(registryResult),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -138,14 +143,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn().mockRejectedValue(new Error('Algolia failed')),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn().mockRejectedValue(new Error('Registry failed')),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -173,14 +180,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn().mockRejectedValue(new Error('Algolia failed')),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn().mockResolvedValue(registryResult),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -214,7 +223,8 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn().mockRejectedValue(new Error('Persistent failure')),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
@@ -223,7 +233,8 @@ describe('useRegistrySearchGateway', () => {
.mockResolvedValue({ nodePacks: [], querySuggestions: [] }),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -242,14 +253,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -271,14 +284,16 @@ describe('useRegistrySearchGateway', () => {
throw new Error('Cache clear failed')
}),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -307,14 +322,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue(algoliaFields)
getSortableFields: vi.fn().mockReturnValue(algoliaFields),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -338,7 +355,8 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn().mockRejectedValue(new Error('Algolia failed')),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue(algoliaFields)
getSortableFields: vi.fn().mockReturnValue(algoliaFields),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
@@ -347,7 +365,8 @@ describe('useRegistrySearchGateway', () => {
.mockResolvedValue({ nodePacks: [], querySuggestions: [] }),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue(registryFields)
getSortableFields: vi.fn().mockReturnValue(registryFields),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -372,14 +391,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn().mockReturnValue(100),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn(),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
@@ -412,14 +433,16 @@ describe('useRegistrySearchGateway', () => {
searchPacks: vi.fn().mockRejectedValue(algoliaError),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
const mockRegistryProvider = {
searchPacks: vi.fn().mockResolvedValue(registryResult),
clearSearchCache: vi.fn(),
getSortValue: vi.fn(),
getSortableFields: vi.fn().mockReturnValue([])
getSortableFields: vi.fn().mockReturnValue([]),
getFilterableFields: vi.fn().mockReturnValue([])
}
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)