Skip to content

[Manager] Implement search filters #4193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/components/dialog/content/manager/ManagerDialogContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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()

Expand Down Expand Up @@ -181,7 +185,9 @@ const {
searchMode,
sortField,
suggestions,
sortOptions
sortOptions,
activeFilters,
filterOptions
} = useRegistrySearch({
initialSortField: initialState.sortField,
initialSearchMode: initialState.searchMode,
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,36 @@
@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')"
/>
<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>
<!-- Add search refinement dropdowns if provider supports them -->
<div v-if="filterOptions?.length" class="flex gap-3 ml-1 text-sm">
<SearchFilterDropdown
v-model:modelValue="sortField"
:options="availableSortOptions"
:label="$t('g.sort')"
v-for="filterOption in filterOptions"
:key="filterOption.id"
v-model:modelValue="selectedFilters[filterOption.id]"
:options="availableFilterOptions(filterOption)"
:label="filterOption.label"
/>
</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>
</div>
</div>
</div>
</template>
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
57 changes: 32 additions & 25 deletions src/composables/useRegistrySearch.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -40,50 +42,48 @@ 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']
)

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
Expand All @@ -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,
Expand All @@ -103,6 +104,10 @@ export function useRegistrySearch(
return getSortableFields()
})

const filterOptions = computed(() => {
return getFilterableFields()
})

return {
isLoading,
pageNumber,
Expand All @@ -113,6 +118,8 @@ export function useRegistrySearch(
suggestions,
searchResults,
sortOptions,
activeFilters,
filterOptions,
clearCache: clearSearchCache
}
}
Loading