diff --git a/src/components/dialog/content/manager/ManagerDialogContent.vue b/src/components/dialog/content/manager/ManagerDialogContent.vue index 34f500243..cf05ee6f1 100644 --- a/src/components/dialog/content/manager/ManagerDialogContent.vue +++ b/src/components/dialog/content/manager/ManagerDialogContent.vue @@ -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" />
{ }) 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') diff --git a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue index d5782bcc5..6fa1cf54f 100644 --- a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue +++ b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue @@ -25,23 +25,78 @@ @option-select="onOptionSelect" />
-
-
- - +
+
+
+ + +
+
+ + {{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }} + +
-
- - {{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }} - + +
+
@@ -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('searchQuery') const searchMode = defineModel('searchMode', { default: 'packs' }) const sortField = defineModel('sortField', { - default: SortableAlgoliaField.Downloads + default: 'total_install' +}) +const selectedFilters = defineModel('activeFilters', { + default: () => ({}) }) const { t } = useI18n() @@ -92,7 +152,7 @@ const availableSortOptions = computed[]>(() => { label: field.label })) }) -const filterOptions: SearchOption[] = [ +const searchModeOptions: SearchOption[] = [ { id: 'packs', label: t('manager.filter.nodePack') }, { id: 'nodes', label: t('g.nodes') } ] diff --git a/src/composables/useRegistrySearch.ts b/src/composables/useRegistrySearch.ts index 001894b27..2bf06b10c 100644 --- a/src/composables/useRegistrySearch.ts +++ b/src/composables/useRegistrySearch.ts @@ -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([]) const suggestions = ref([]) + const activeFilters = ref({}) const searchAttributes = computed(() => 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 } } diff --git a/src/services/gateway/registrySearchGateway.ts b/src/services/gateway/registrySearchGateway.ts index 834e730a8..32fee6b9a 100644 --- a/src/services/gateway/registrySearchGateway.ts +++ b/src/services/gateway/registrySearchGateway.ts @@ -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 } } diff --git a/src/services/providers/algoliaSearchProvider.ts b/src/services/providers/algoliaSearchProvider.ts index 71da739ae..934bc08f0 100644 --- a/src/services/providers/algoliaSearchProvider.ts +++ b/src/services/providers/algoliaSearchProvider.ts @@ -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 => { 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 } } diff --git a/src/services/providers/registrySearchProvider.ts b/src/services/providers/registrySearchProvider.ts index bea726a2e..f3ee58c4e 100644 --- a/src/services/providers/registrySearchProvider.ts +++ b/src/services/providers/registrySearchProvider.ts @@ -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 => { - 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 } } diff --git a/src/types/algoliaTypes.ts b/src/types/algoliaTypes.ts index 009174411..07300a839 100644 --- a/src/types/algoliaTypes.ts +++ b/src/types/algoliaTypes.ts @@ -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 + sortField?: string + sortDirection?: 'asc' | 'desc' } diff --git a/src/types/searchServiceTypes.ts b/src/types/searchServiceTypes.ts index 768770f24..030a90763 100644 --- a/src/types/searchServiceTypes.ts +++ b/src/types/searchServiceTypes.ts @@ -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 + 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[] } diff --git a/tests-ui/tests/services/registrySearchGateway.test.ts b/tests-ui/tests/services/registrySearchGateway.test.ts index e51b164d1..824be6fd7 100644 --- a/tests-ui/tests/services/registrySearchGateway.test.ts +++ b/tests-ui/tests/services/registrySearchGateway.test.ts @@ -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)