mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 09:44:06 +00:00
feat(settings): improve search to include nav items and show all results (#9195)
## Summary - Settings search now matches sidebar navigation items (Keybinding, About, Extension, etc.) and navigates to the corresponding panel - Search results show all matching settings across all categories instead of filtering to only the first matching category - Search result group headers display parent category prefix (e.g. "LiteGraph › Node") for clarity ## Test plan - [x] Search "Keybinding" → sidebar highlights and navigates to Keybinding panel - [x] Search "badge" → shows all 4 badge settings (3 LiteGraph + 1 Comfy) - [x] Search "canvas" → shows results from all categories - [x] Clear search → returns to default category - [x] Unit tests pass (`pnpm test:unit`) <img width="1425" height="682" alt="스크린샷 2026-02-25 오후 3 01 05" src="https://github.com/user-attachments/assets/956c4635-b140-4dff-8145-db312d295160" /> 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9195-feat-settings-improve-search-to-include-nav-items-and-show-all-results-3126d73d3650814dbf3ce1d59ad962cf) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -44,17 +44,7 @@
|
||||
<template #header />
|
||||
|
||||
<template #content>
|
||||
<template v-if="inSearch">
|
||||
<SettingsPanel :setting-groups="searchResults" />
|
||||
</template>
|
||||
<template v-else-if="activeSettingCategory">
|
||||
<CurrentUserMessage v-if="activeSettingCategory.label === 'Comfy'" />
|
||||
<ColorPaletteMessage
|
||||
v-if="activeSettingCategory.label === 'Appearance'"
|
||||
/>
|
||||
<SettingsPanel :setting-groups="sortedGroups(activeSettingCategory)" />
|
||||
</template>
|
||||
<template v-else-if="activePanel">
|
||||
<template v-if="activePanel">
|
||||
<Suspense>
|
||||
<component :is="activePanel.component" v-bind="activePanel.props" />
|
||||
<template #fallback>
|
||||
@@ -64,6 +54,16 @@
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
<template v-else-if="inSearch">
|
||||
<SettingsPanel :setting-groups="searchResults" />
|
||||
</template>
|
||||
<template v-else-if="activeSettingCategory">
|
||||
<CurrentUserMessage v-if="activeSettingCategory.label === 'Comfy'" />
|
||||
<ColorPaletteMessage
|
||||
v-if="activeSettingCategory.label === 'Appearance'"
|
||||
/>
|
||||
<SettingsPanel :setting-groups="sortedGroups(activeSettingCategory)" />
|
||||
</template>
|
||||
</template>
|
||||
</BaseModalLayout>
|
||||
</template>
|
||||
@@ -110,6 +110,7 @@ const {
|
||||
searchQuery,
|
||||
inSearch,
|
||||
searchResultsCategories,
|
||||
matchedNavItemKeys,
|
||||
handleSearch: handleSearchBase,
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
@@ -119,16 +120,29 @@ const authActions = useFirebaseAuthActions()
|
||||
const navRef = ref<HTMLElement | null>(null)
|
||||
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
|
||||
|
||||
watch(searchResultsCategories, (categories) => {
|
||||
if (!inSearch.value || categories.size === 0) return
|
||||
const firstMatch = navGroups.value
|
||||
.flatMap((g) => g.items)
|
||||
.find((item) => {
|
||||
const node = findCategoryByKey(item.id)
|
||||
return node && categories.has(node.label)
|
||||
})
|
||||
activeCategoryKey.value = firstMatch?.id ?? null
|
||||
})
|
||||
const searchableNavItems = computed(() =>
|
||||
navGroups.value.flatMap((g) =>
|
||||
g.items.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.label
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
watch(
|
||||
[searchResultsCategories, matchedNavItemKeys],
|
||||
([categories, navKeys]) => {
|
||||
if (!inSearch.value || (categories.size === 0 && navKeys.size === 0)) return
|
||||
const firstMatch = navGroups.value
|
||||
.flatMap((g) => g.items)
|
||||
.find((item) => {
|
||||
if (navKeys.has(item.id)) return true
|
||||
const node = findCategoryByKey(item.id)
|
||||
return node && categories.has(node.label)
|
||||
})
|
||||
activeCategoryKey.value = firstMatch?.id ?? null
|
||||
}
|
||||
)
|
||||
|
||||
const activeSettingCategory = computed<SettingTreeNode | null>(() => {
|
||||
if (!activeCategoryKey.value) return null
|
||||
@@ -163,7 +177,7 @@ function sortedGroups(category: SettingTreeNode): ISettingGroup[] {
|
||||
}
|
||||
|
||||
function handleSearch(query: string) {
|
||||
handleSearchBase(query.trim())
|
||||
handleSearchBase(query.trim(), searchableNavItems.value)
|
||||
if (query) {
|
||||
activeCategoryKey.value = null
|
||||
} else if (!activeCategoryKey.value) {
|
||||
@@ -175,12 +189,7 @@ function onNavItemClick(id: string) {
|
||||
activeCategoryKey.value = id
|
||||
}
|
||||
|
||||
const searchResults = computed<ISettingGroup[]>(() => {
|
||||
const category = activeCategoryKey.value
|
||||
? findCategoryByKey(activeCategoryKey.value)
|
||||
: null
|
||||
return getSearchResults(category)
|
||||
})
|
||||
const searchResults = computed<ISettingGroup[]>(() => getSearchResults(null))
|
||||
|
||||
// Scroll to and highlight the target setting once the correct category renders.
|
||||
if (scrollToSettingId) {
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
<div class="setting-group">
|
||||
<Divider v-if="divider" />
|
||||
<h3>
|
||||
<span v-if="group.category" class="text-muted">
|
||||
{{
|
||||
$t(
|
||||
`settingsCategories.${normalizeI18nKey(group.category)}`,
|
||||
group.category
|
||||
)
|
||||
}}
|
||||
›
|
||||
</span>
|
||||
{{
|
||||
$t(`settingsCategories.${normalizeI18nKey(group.label)}`, group.label)
|
||||
}}
|
||||
@@ -27,6 +36,7 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
defineProps<{
|
||||
group: {
|
||||
label: string
|
||||
category?: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
divider?: boolean
|
||||
|
||||
@@ -299,10 +299,12 @@ describe('useSettingSearch', () => {
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
category: 'Category',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
category: 'Category',
|
||||
settings: [mockSettings['Category.Setting2']]
|
||||
}
|
||||
])
|
||||
@@ -332,15 +334,50 @@ describe('useSettingSearch', () => {
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
category: 'Category',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
},
|
||||
{
|
||||
label: 'SubCategory',
|
||||
category: 'Other',
|
||||
settings: [mockSettings['Other.Setting3']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns results from all categories when searching cross-category term', () => {
|
||||
// Simulates the "badge" scenario: same term matches settings in
|
||||
// multiple categories (e.g. LiteGraph and Comfy)
|
||||
mockSettings['LiteGraph.BadgeSetting'] = {
|
||||
id: 'LiteGraph.BadgeSetting',
|
||||
name: 'Node source badge mode',
|
||||
type: 'combo',
|
||||
defaultValue: 'default',
|
||||
category: ['LiteGraph', 'Node']
|
||||
}
|
||||
mockSettings['Comfy.BadgeSetting'] = {
|
||||
id: 'Comfy.BadgeSetting',
|
||||
name: 'Show API node pricing badge',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
category: ['Comfy', 'API Nodes']
|
||||
}
|
||||
|
||||
const search = useSettingSearch()
|
||||
search.handleSearch('badge')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain(
|
||||
'LiteGraph.BadgeSetting'
|
||||
)
|
||||
expect(search.filteredSettingIds.value).toContain('Comfy.BadgeSetting')
|
||||
|
||||
// getSearchResults(null) should return both categories' results
|
||||
const results = search.getSearchResults(null)
|
||||
const labels = results.map((g) => g.label)
|
||||
expect(labels).toContain('Node')
|
||||
expect(labels).toContain('API Nodes')
|
||||
})
|
||||
|
||||
it('returns empty array when no filtered results', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = []
|
||||
@@ -372,6 +409,7 @@ describe('useSettingSearch', () => {
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
category: 'Category',
|
||||
settings: [
|
||||
mockSettings['Category.Setting1'],
|
||||
mockSettings['Category.Setting4']
|
||||
@@ -381,6 +419,75 @@ describe('useSettingSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('nav item matching', () => {
|
||||
const navItems = [
|
||||
{ key: 'keybinding', label: 'Keybinding' },
|
||||
{ key: 'about', label: 'About' },
|
||||
{ key: 'extension', label: 'Extension' },
|
||||
{ key: 'Comfy', label: 'Comfy' }
|
||||
]
|
||||
|
||||
it('matches nav items by key', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('keybinding', navItems)
|
||||
|
||||
expect(search.matchedNavItemKeys.value.has('keybinding')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches nav items by translated label (case insensitive)', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('ABOUT', navItems)
|
||||
|
||||
expect(search.matchedNavItemKeys.value.has('about')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches partial nav item labels', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('ext', navItems)
|
||||
|
||||
expect(search.matchedNavItemKeys.value.has('extension')).toBe(true)
|
||||
})
|
||||
|
||||
it('clears matched nav item keys on empty query', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('keybinding', navItems)
|
||||
expect(search.matchedNavItemKeys.value.size).toBeGreaterThan(0)
|
||||
|
||||
search.handleSearch('', navItems)
|
||||
expect(search.matchedNavItemKeys.value.size).toBe(0)
|
||||
})
|
||||
|
||||
it('can match both settings and nav items simultaneously', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('other', navItems)
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Other.Setting3')
|
||||
expect(search.matchedNavItemKeys.value.size).toBe(0)
|
||||
})
|
||||
|
||||
it('matches nav items with translated labels different from key', () => {
|
||||
const translatedNavItems = [{ key: 'keybinding', label: '키 바인딩' }]
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('키 바인딩', translatedNavItems)
|
||||
|
||||
expect(search.matchedNavItemKeys.value.has('keybinding')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match nav items when no nav items provided', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('keybinding')
|
||||
|
||||
expect(search.matchedNavItemKeys.value.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty settings store', () => {
|
||||
mockSettingStore.settingsById = {}
|
||||
|
||||
@@ -10,12 +10,18 @@ import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
|
||||
interface SearchableNavItem {
|
||||
key: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export function useSettingSearch() {
|
||||
const settingStore = useSettingStore()
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
const filteredSettingIds = ref<string[]>([])
|
||||
const matchedNavItemKeys = ref<Set<string>>(new Set())
|
||||
const searchInProgress = ref<boolean>(false)
|
||||
|
||||
watch(searchQuery, () => (searchInProgress.value = true))
|
||||
@@ -46,7 +52,9 @@ export function useSettingSearch() {
|
||||
/**
|
||||
* Handle search functionality
|
||||
*/
|
||||
const handleSearch = (query: string) => {
|
||||
const handleSearch = (query: string, navItems?: SearchableNavItem[]) => {
|
||||
matchedNavItemKeys.value = new Set()
|
||||
|
||||
if (!query) {
|
||||
filteredSettingIds.value = []
|
||||
return
|
||||
@@ -89,6 +97,17 @@ export function useSettingSearch() {
|
||||
)
|
||||
})
|
||||
|
||||
if (navItems) {
|
||||
for (const item of navItems) {
|
||||
if (
|
||||
item.key.toLocaleLowerCase().includes(queryLower) ||
|
||||
item.label.toLocaleLowerCase().includes(queryLower)
|
||||
) {
|
||||
matchedNavItemKeys.value.add(item.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredSettingIds.value = filteredSettings.map((x) => x.id)
|
||||
searchInProgress.value = false
|
||||
}
|
||||
@@ -99,30 +118,42 @@ export function useSettingSearch() {
|
||||
const getSearchResults = (
|
||||
activeCategory: SettingTreeNode | null
|
||||
): ISettingGroup[] => {
|
||||
const groupedSettings: { [key: string]: SettingParams[] } = {}
|
||||
const groupedSettings: {
|
||||
[key: string]: { category: string; settings: SettingParams[] }
|
||||
} = {}
|
||||
|
||||
filteredSettingIds.value.forEach((id) => {
|
||||
const setting = settingStore.settingsById[id]
|
||||
const info = getSettingInfo(setting)
|
||||
const groupLabel = info.subCategory
|
||||
const groupKey =
|
||||
activeCategory === null
|
||||
? `${info.category}/${info.subCategory}`
|
||||
: info.subCategory
|
||||
|
||||
if (activeCategory === null || activeCategory.label === info.category) {
|
||||
if (!groupedSettings[groupLabel]) {
|
||||
groupedSettings[groupLabel] = []
|
||||
if (!groupedSettings[groupKey]) {
|
||||
groupedSettings[groupKey] = {
|
||||
category: info.category,
|
||||
settings: []
|
||||
}
|
||||
}
|
||||
groupedSettings[groupLabel].push(setting)
|
||||
groupedSettings[groupKey].settings.push(setting)
|
||||
}
|
||||
})
|
||||
|
||||
return Object.entries(groupedSettings).map(([label, settings]) => ({
|
||||
label,
|
||||
settings
|
||||
}))
|
||||
return Object.entries(groupedSettings).map(
|
||||
([key, { category, settings }]) => ({
|
||||
label: activeCategory === null ? key.split('/')[1] : key,
|
||||
...(activeCategory === null ? { category } : {}),
|
||||
settings
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
filteredSettingIds,
|
||||
matchedNavItemKeys,
|
||||
searchInProgress,
|
||||
searchResultsCategories,
|
||||
queryIsEmpty,
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface FormItem {
|
||||
|
||||
export interface ISettingGroup {
|
||||
label: string
|
||||
category?: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user