mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
5 Commits
feature/qu
...
manager/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f08b9aa9eb | ||
|
|
2dbb237a83 | ||
|
|
85f0aca045 | ||
|
|
6eed618a94 | ||
|
|
3f036fcf51 |
190
browser_tests/assets/save_animated_png.json
Normal file
190
browser_tests/assets/save_animated_png.json
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"id": "cffcce2d-a13c-4a5f-929b-82f274bacc36",
|
||||
"revision": 0,
|
||||
"last_node_id": 14,
|
||||
"last_link_id": 14,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-361.02374267578125,
|
||||
-40.05255126953125
|
||||
],
|
||||
"size": [
|
||||
274.080078125,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI_00137_.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "ImageBatch",
|
||||
"pos": [
|
||||
146.92184448242188,
|
||||
104.8472671508789
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image1",
|
||||
"type": "IMAGE",
|
||||
"link": 11
|
||||
},
|
||||
{
|
||||
"name": "image2",
|
||||
"type": "IMAGE",
|
||||
"link": 12
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
14
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageBatch"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "SaveAnimatedPNG",
|
||||
"pos": [
|
||||
457.4212646484375,
|
||||
39.56276321411133
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
368
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 14
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI",
|
||||
6,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-360.4931640625,
|
||||
326.1943664550781
|
||||
],
|
||||
"size": [
|
||||
274.080078125,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI_00153_.png",
|
||||
"image"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
11,
|
||||
10,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
12,
|
||||
11,
|
||||
0,
|
||||
12,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
14,
|
||||
12,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.129559245649766,
|
||||
"offset": [
|
||||
768.6140137916129,
|
||||
203.6152852376302
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.22.2"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -25,23 +25,35 @@
|
||||
@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">
|
||||
<SearchFilterDropdown
|
||||
v-for="filterOption in filterOptions"
|
||||
:key="filterOption.id"
|
||||
v-model:modelValue="selectedFilters[filterOption.id]"
|
||||
:options="availableFilterOptions(filterOption)"
|
||||
:label="filterOption.label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,27 +68,30 @@ 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,11 +107,22 @@ 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') }
|
||||
]
|
||||
|
||||
// Convert filter options to SearchOption format for SearchFilterDropdown
|
||||
const availableFilterOptions = (
|
||||
filter: SearchFilter
|
||||
): SearchOption<string>[] => {
|
||||
if (!filter.options) return []
|
||||
return filter.options.map((option) => ({
|
||||
id: option.value,
|
||||
label: option.label
|
||||
}))
|
||||
}
|
||||
|
||||
// When a dropdown query suggestion is selected, update the search query
|
||||
const onOptionSelect = (event: AutoCompleteOptionSelectEvent) => {
|
||||
searchQuery.value = event.value.query
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:options="options"
|
||||
option-label="label"
|
||||
option-value="id"
|
||||
placeholder="Any"
|
||||
class="min-w-[6rem] border-none bg-transparent shadow-none"
|
||||
:pt="{
|
||||
input: { class: 'py-0 px-1 border-none' },
|
||||
|
||||
@@ -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,10 @@ export function useRegistrySearch(
|
||||
return getSortableFields()
|
||||
})
|
||||
|
||||
const filterOptions = computed(() => {
|
||||
return getFilterableFields()
|
||||
})
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
pageNumber,
|
||||
@@ -113,6 +118,8 @@ export function useRegistrySearch(
|
||||
suggestions,
|
||||
searchResults,
|
||||
sortOptions,
|
||||
activeFilters,
|
||||
filterOptions,
|
||||
clearCache: clearSearchCache
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
|
||||
import { useComfyRegistrySearchProvider } from '@/services/providers/registrySearchProvider'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { SearchNodePacksParams } from '@/types/algoliaTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type {
|
||||
NodePackSearchProvider,
|
||||
ProviderState,
|
||||
SearchPacksResult
|
||||
} from '@/types/searchServiceTypes'
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
|
||||
interface ProviderState {
|
||||
provider: NodePackSearchProvider
|
||||
name: string
|
||||
isHealthy: boolean
|
||||
lastError?: Error
|
||||
lastAttempt?: Date
|
||||
consecutiveFailures: number
|
||||
}
|
||||
|
||||
const CIRCUIT_BREAKER_THRESHOLD = 3 // Number of failures before circuit opens
|
||||
const CIRCUIT_BREAKER_TIMEOUT = 60000 // 1 minute before retry
|
||||
|
||||
@@ -32,27 +25,31 @@ 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
|
||||
const store = useComfyRegistryStore()
|
||||
|
||||
// Initialize providers in priority order
|
||||
try {
|
||||
providers.push({
|
||||
provider: useAlgoliaSearchProvider(),
|
||||
name: 'Algolia',
|
||||
// Initialize providers only once
|
||||
if (!store.isSearchGatewayInitialized) {
|
||||
// Initialize providers in priority order
|
||||
try {
|
||||
store.searchProviders.push({
|
||||
provider: useAlgoliaSearchProvider(),
|
||||
name: 'Algolia',
|
||||
isHealthy: true,
|
||||
consecutiveFailures: 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize Algolia provider:', error)
|
||||
}
|
||||
|
||||
store.searchProviders.push({
|
||||
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
|
||||
})
|
||||
store.isSearchGatewayInitialized = true
|
||||
}
|
||||
|
||||
// TODO: Add an "offline" provider that operates on a local cache of the registry.
|
||||
|
||||
@@ -109,16 +106,17 @@ 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 =
|
||||
store.searchProviders[store.activeSearchProviderIndex]
|
||||
if (currentProvider && isCircuitClosed(currentProvider)) {
|
||||
return currentProvider.provider
|
||||
}
|
||||
|
||||
// Otherwise, find the first healthy provider
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
const providerState = providers[i]
|
||||
for (let i = 0; i < store.searchProviders.length; i++) {
|
||||
const providerState = store.searchProviders[i]
|
||||
if (isCircuitClosed(providerState)) {
|
||||
activeProviderIndex = i
|
||||
store.activeSearchProviderIndex = i
|
||||
return providerState.provider
|
||||
}
|
||||
}
|
||||
@@ -131,8 +129,8 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
|
||||
* Move to the next provider if available.
|
||||
*/
|
||||
const updateActiveProviderOnFailure = () => {
|
||||
if (activeProviderIndex < providers.length - 1) {
|
||||
activeProviderIndex++
|
||||
if (store.activeSearchProviderIndex < store.searchProviders.length - 1) {
|
||||
store.activeSearchProviderIndex++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,17 +144,23 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
|
||||
let lastError: Error | null = null
|
||||
|
||||
// Start with the current active provider
|
||||
for (let attempts = 0; attempts < providers.length; attempts++) {
|
||||
for (
|
||||
let attempts = 0;
|
||||
attempts < store.searchProviders.length;
|
||||
attempts++
|
||||
) {
|
||||
try {
|
||||
const provider = getActiveProvider()
|
||||
const providerState = providers[activeProviderIndex]
|
||||
const providerState =
|
||||
store.searchProviders[store.activeSearchProviderIndex]
|
||||
|
||||
const result = await provider.searchPacks(query, params)
|
||||
recordSuccess(providerState)
|
||||
return result
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
const providerState = providers[activeProviderIndex]
|
||||
const providerState =
|
||||
store.searchProviders[store.activeSearchProviderIndex]
|
||||
recordFailure(providerState, lastError)
|
||||
console.warn(
|
||||
`${providerState.name} search provider failed (${providerState.consecutiveFailures} failures):`,
|
||||
@@ -178,7 +182,7 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
|
||||
* Clear the search cache for all providers that implement it.
|
||||
*/
|
||||
const clearSearchCache = () => {
|
||||
for (const providerState of providers) {
|
||||
for (const providerState of store.searchProviders) {
|
||||
try {
|
||||
providerState.provider.clearSearchCache()
|
||||
} catch (error) {
|
||||
@@ -218,10 +222,19 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
|
||||
return getActiveProvider().getSortableFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filterable fields for the active provider.
|
||||
*/
|
||||
const getFilterableFields = () => {
|
||||
const provider = getActiveProvider()
|
||||
return provider.getFilterableFields()
|
||||
}
|
||||
|
||||
return {
|
||||
searchPacks,
|
||||
clearSearchCache,
|
||||
getSortValue,
|
||||
getSortableFields
|
||||
getSortableFields,
|
||||
getFilterableFields
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,76 @@ 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) {
|
||||
// Validate sort field to prevent malformed API requests
|
||||
if (!/^[a-zA-Z_]+$/.test(sortField)) {
|
||||
throw new Error(`Invalid sort field: ${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 +120,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 +137,114 @@ 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 - check for additional patterns
|
||||
for (const device of stats.devices) {
|
||||
const deviceType = device.type.toLowerCase()
|
||||
if (deviceType.includes('nvidia') || deviceType.includes('cuda'))
|
||||
return 'cuda'
|
||||
if (deviceType.includes('apple') || deviceType.includes('mps'))
|
||||
return 'mps'
|
||||
if (deviceType.includes('amd') || 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 +252,7 @@ export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
|
||||
searchPacks,
|
||||
clearSearchCache,
|
||||
getSortValue,
|
||||
getSortableFields
|
||||
getSortableFields,
|
||||
getFilterableFields
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
import { partition } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import type { ProviderState } from '@/types/searchServiceTypes'
|
||||
|
||||
const PACK_LIST_CACHE_SIZE = 20
|
||||
const PACK_BY_ID_CACHE_SIZE = 64
|
||||
@@ -34,6 +36,11 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
maxSize: PACK_BY_ID_CACHE_SIZE
|
||||
})
|
||||
|
||||
// Search gateway state
|
||||
const searchProviders = ref<ProviderState[]>([])
|
||||
const activeSearchProviderIndex = ref(0)
|
||||
const isSearchGatewayInitialized = ref(false)
|
||||
|
||||
/**
|
||||
* Get a list of all node packs from the registry
|
||||
*/
|
||||
@@ -137,6 +144,11 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
cancelRequests,
|
||||
|
||||
isLoading: registryService.isLoading,
|
||||
error: registryService.error
|
||||
error: registryService.error,
|
||||
|
||||
// Search gateway state
|
||||
searchProviders,
|
||||
activeSearchProviderIndex,
|
||||
isSearchGatewayInitialized
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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: 'single-select' | 'boolean'
|
||||
options?: FilterOption[]
|
||||
defaultValue?: string | boolean
|
||||
}
|
||||
|
||||
export interface FilterOption {
|
||||
value: string
|
||||
label: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export type ActiveFilters = Record<string, string | boolean>
|
||||
|
||||
export interface SearchPacksResult {
|
||||
nodePacks: RegistryNodePack[]
|
||||
querySuggestions: QuerySuggestion[]
|
||||
@@ -46,4 +63,22 @@ 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[]
|
||||
}
|
||||
|
||||
/**
|
||||
* State of a search provider
|
||||
*/
|
||||
export interface ProviderState {
|
||||
provider: NodePackSearchProvider
|
||||
name: string
|
||||
isHealthy: boolean
|
||||
lastError?: Error
|
||||
lastAttempt?: Date
|
||||
consecutiveFailures: number
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
|
||||
@@ -14,6 +15,7 @@ describe('useRegistrySearchGateway', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
||||
})
|
||||
@@ -30,14 +32,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 +67,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 +106,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 +145,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 +182,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 +225,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 +235,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 +255,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 +286,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 +324,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 +357,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 +367,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 +393,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 +435,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)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useComfyRegistrySearchProvider } from '@/services/providers/registrySearchProvider'
|
||||
@@ -14,6 +15,7 @@ describe('useComfyRegistrySearchProvider', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Setup store mock
|
||||
vi.mocked(useComfyRegistryStore).mockReturnValue({
|
||||
@@ -45,7 +47,7 @@ describe('useComfyRegistrySearchProvider', () => {
|
||||
search: 'test',
|
||||
comfy_node_search: undefined,
|
||||
limit: 10,
|
||||
offset: 0
|
||||
page: 1
|
||||
})
|
||||
expect(result.nodePacks).toEqual(mockResults.nodes)
|
||||
expect(result.querySuggestions).toEqual([])
|
||||
@@ -68,7 +70,7 @@ describe('useComfyRegistrySearchProvider', () => {
|
||||
search: undefined,
|
||||
comfy_node_search: 'LoadImage',
|
||||
limit: 20,
|
||||
offset: 20
|
||||
page: 2
|
||||
})
|
||||
expect(result.nodePacks).toEqual(mockResults.nodes)
|
||||
})
|
||||
@@ -136,7 +138,7 @@ describe('useComfyRegistrySearchProvider', () => {
|
||||
|
||||
it('should return download count for downloads field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
expect(provider.getSortValue(testPack, 'downloads')).toBe(100)
|
||||
expect(provider.getSortValue(testPack, 'total_install')).toBe(100)
|
||||
})
|
||||
|
||||
it('should return pack name for name field', () => {
|
||||
@@ -146,22 +148,24 @@ describe('useComfyRegistrySearchProvider', () => {
|
||||
|
||||
it('should return publisher name for publisher field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
expect(provider.getSortValue(testPack, 'publisher')).toBe('Publisher One')
|
||||
expect(provider.getSortValue(testPack, 'publisher_name')).toBe(
|
||||
'Publisher One'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return timestamp for updated field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const timestamp = new Date('2024-01-15T10:00:00Z').getTime()
|
||||
expect(provider.getSortValue(testPack, 'updated')).toBe(timestamp)
|
||||
expect(provider.getSortValue(testPack, 'last_updated')).toBe(timestamp)
|
||||
})
|
||||
|
||||
it('should handle missing values gracefully', () => {
|
||||
const incompletePack = { id: '1', name: 'Incomplete' }
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
|
||||
expect(provider.getSortValue(incompletePack, 'downloads')).toBe(0)
|
||||
expect(provider.getSortValue(incompletePack, 'publisher')).toBe('')
|
||||
expect(provider.getSortValue(incompletePack, 'updated')).toBe(0)
|
||||
expect(provider.getSortValue(incompletePack, 'total_install')).toBe(0)
|
||||
expect(provider.getSortValue(incompletePack, 'publisher_name')).toBe('')
|
||||
expect(provider.getSortValue(incompletePack, 'last_updated')).toBe(0)
|
||||
})
|
||||
|
||||
it('should return 0 for unknown sort fields', () => {
|
||||
@@ -176,10 +180,10 @@ describe('useComfyRegistrySearchProvider', () => {
|
||||
const fields = provider.getSortableFields()
|
||||
|
||||
expect(fields).toEqual([
|
||||
{ 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' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user