mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +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 #header />
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<template v-if="inSearch">
|
<template v-if="activePanel">
|
||||||
<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">
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<component :is="activePanel.component" v-bind="activePanel.props" />
|
<component :is="activePanel.component" v-bind="activePanel.props" />
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
@@ -64,6 +54,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</template>
|
</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>
|
</template>
|
||||||
</BaseModalLayout>
|
</BaseModalLayout>
|
||||||
</template>
|
</template>
|
||||||
@@ -110,6 +110,7 @@ const {
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
inSearch,
|
inSearch,
|
||||||
searchResultsCategories,
|
searchResultsCategories,
|
||||||
|
matchedNavItemKeys,
|
||||||
handleSearch: handleSearchBase,
|
handleSearch: handleSearchBase,
|
||||||
getSearchResults
|
getSearchResults
|
||||||
} = useSettingSearch()
|
} = useSettingSearch()
|
||||||
@@ -119,16 +120,29 @@ const authActions = useFirebaseAuthActions()
|
|||||||
const navRef = ref<HTMLElement | null>(null)
|
const navRef = ref<HTMLElement | null>(null)
|
||||||
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
|
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
|
||||||
|
|
||||||
watch(searchResultsCategories, (categories) => {
|
const searchableNavItems = computed(() =>
|
||||||
if (!inSearch.value || categories.size === 0) return
|
navGroups.value.flatMap((g) =>
|
||||||
const firstMatch = navGroups.value
|
g.items.map((item) => ({
|
||||||
.flatMap((g) => g.items)
|
key: item.id,
|
||||||
.find((item) => {
|
label: item.label
|
||||||
const node = findCategoryByKey(item.id)
|
}))
|
||||||
return node && categories.has(node.label)
|
)
|
||||||
})
|
)
|
||||||
activeCategoryKey.value = firstMatch?.id ?? null
|
|
||||||
})
|
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>(() => {
|
const activeSettingCategory = computed<SettingTreeNode | null>(() => {
|
||||||
if (!activeCategoryKey.value) return null
|
if (!activeCategoryKey.value) return null
|
||||||
@@ -163,7 +177,7 @@ function sortedGroups(category: SettingTreeNode): ISettingGroup[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch(query: string) {
|
function handleSearch(query: string) {
|
||||||
handleSearchBase(query.trim())
|
handleSearchBase(query.trim(), searchableNavItems.value)
|
||||||
if (query) {
|
if (query) {
|
||||||
activeCategoryKey.value = null
|
activeCategoryKey.value = null
|
||||||
} else if (!activeCategoryKey.value) {
|
} else if (!activeCategoryKey.value) {
|
||||||
@@ -175,12 +189,7 @@ function onNavItemClick(id: string) {
|
|||||||
activeCategoryKey.value = id
|
activeCategoryKey.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResults = computed<ISettingGroup[]>(() => {
|
const searchResults = computed<ISettingGroup[]>(() => getSearchResults(null))
|
||||||
const category = activeCategoryKey.value
|
|
||||||
? findCategoryByKey(activeCategoryKey.value)
|
|
||||||
: null
|
|
||||||
return getSearchResults(category)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Scroll to and highlight the target setting once the correct category renders.
|
// Scroll to and highlight the target setting once the correct category renders.
|
||||||
if (scrollToSettingId) {
|
if (scrollToSettingId) {
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<Divider v-if="divider" />
|
<Divider v-if="divider" />
|
||||||
<h3>
|
<h3>
|
||||||
|
<span v-if="group.category" class="text-muted">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
`settingsCategories.${normalizeI18nKey(group.category)}`,
|
||||||
|
group.category
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
›
|
||||||
|
</span>
|
||||||
{{
|
{{
|
||||||
$t(`settingsCategories.${normalizeI18nKey(group.label)}`, group.label)
|
$t(`settingsCategories.${normalizeI18nKey(group.label)}`, group.label)
|
||||||
}}
|
}}
|
||||||
@@ -27,6 +36,7 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
group: {
|
group: {
|
||||||
label: string
|
label: string
|
||||||
|
category?: string
|
||||||
settings: SettingParams[]
|
settings: SettingParams[]
|
||||||
}
|
}
|
||||||
divider?: boolean
|
divider?: boolean
|
||||||
|
|||||||
@@ -299,10 +299,12 @@ describe('useSettingSearch', () => {
|
|||||||
expect(results).toEqual([
|
expect(results).toEqual([
|
||||||
{
|
{
|
||||||
label: 'Basic',
|
label: 'Basic',
|
||||||
|
category: 'Category',
|
||||||
settings: [mockSettings['Category.Setting1']]
|
settings: [mockSettings['Category.Setting1']]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Advanced',
|
label: 'Advanced',
|
||||||
|
category: 'Category',
|
||||||
settings: [mockSettings['Category.Setting2']]
|
settings: [mockSettings['Category.Setting2']]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -332,15 +334,50 @@ describe('useSettingSearch', () => {
|
|||||||
expect(results).toEqual([
|
expect(results).toEqual([
|
||||||
{
|
{
|
||||||
label: 'Basic',
|
label: 'Basic',
|
||||||
|
category: 'Category',
|
||||||
settings: [mockSettings['Category.Setting1']]
|
settings: [mockSettings['Category.Setting1']]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'SubCategory',
|
label: 'SubCategory',
|
||||||
|
category: 'Other',
|
||||||
settings: [mockSettings['Other.Setting3']]
|
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', () => {
|
it('returns empty array when no filtered results', () => {
|
||||||
const search = useSettingSearch()
|
const search = useSettingSearch()
|
||||||
search.filteredSettingIds.value = []
|
search.filteredSettingIds.value = []
|
||||||
@@ -372,6 +409,7 @@ describe('useSettingSearch', () => {
|
|||||||
expect(results).toEqual([
|
expect(results).toEqual([
|
||||||
{
|
{
|
||||||
label: 'Basic',
|
label: 'Basic',
|
||||||
|
category: 'Category',
|
||||||
settings: [
|
settings: [
|
||||||
mockSettings['Category.Setting1'],
|
mockSettings['Category.Setting1'],
|
||||||
mockSettings['Category.Setting4']
|
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', () => {
|
describe('edge cases', () => {
|
||||||
it('handles empty settings store', () => {
|
it('handles empty settings store', () => {
|
||||||
mockSettingStore.settingsById = {}
|
mockSettingStore.settingsById = {}
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
|||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
|
|
||||||
|
interface SearchableNavItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useSettingSearch() {
|
export function useSettingSearch() {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
|
|
||||||
const searchQuery = ref<string>('')
|
const searchQuery = ref<string>('')
|
||||||
const filteredSettingIds = ref<string[]>([])
|
const filteredSettingIds = ref<string[]>([])
|
||||||
|
const matchedNavItemKeys = ref<Set<string>>(new Set())
|
||||||
const searchInProgress = ref<boolean>(false)
|
const searchInProgress = ref<boolean>(false)
|
||||||
|
|
||||||
watch(searchQuery, () => (searchInProgress.value = true))
|
watch(searchQuery, () => (searchInProgress.value = true))
|
||||||
@@ -46,7 +52,9 @@ export function useSettingSearch() {
|
|||||||
/**
|
/**
|
||||||
* Handle search functionality
|
* Handle search functionality
|
||||||
*/
|
*/
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = (query: string, navItems?: SearchableNavItem[]) => {
|
||||||
|
matchedNavItemKeys.value = new Set()
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
filteredSettingIds.value = []
|
filteredSettingIds.value = []
|
||||||
return
|
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)
|
filteredSettingIds.value = filteredSettings.map((x) => x.id)
|
||||||
searchInProgress.value = false
|
searchInProgress.value = false
|
||||||
}
|
}
|
||||||
@@ -99,30 +118,42 @@ export function useSettingSearch() {
|
|||||||
const getSearchResults = (
|
const getSearchResults = (
|
||||||
activeCategory: SettingTreeNode | null
|
activeCategory: SettingTreeNode | null
|
||||||
): ISettingGroup[] => {
|
): ISettingGroup[] => {
|
||||||
const groupedSettings: { [key: string]: SettingParams[] } = {}
|
const groupedSettings: {
|
||||||
|
[key: string]: { category: string; settings: SettingParams[] }
|
||||||
|
} = {}
|
||||||
|
|
||||||
filteredSettingIds.value.forEach((id) => {
|
filteredSettingIds.value.forEach((id) => {
|
||||||
const setting = settingStore.settingsById[id]
|
const setting = settingStore.settingsById[id]
|
||||||
const info = getSettingInfo(setting)
|
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 (activeCategory === null || activeCategory.label === info.category) {
|
||||||
if (!groupedSettings[groupLabel]) {
|
if (!groupedSettings[groupKey]) {
|
||||||
groupedSettings[groupLabel] = []
|
groupedSettings[groupKey] = {
|
||||||
|
category: info.category,
|
||||||
|
settings: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
groupedSettings[groupLabel].push(setting)
|
groupedSettings[groupKey].settings.push(setting)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return Object.entries(groupedSettings).map(([label, settings]) => ({
|
return Object.entries(groupedSettings).map(
|
||||||
label,
|
([key, { category, settings }]) => ({
|
||||||
settings
|
label: activeCategory === null ? key.split('/')[1] : key,
|
||||||
}))
|
...(activeCategory === null ? { category } : {}),
|
||||||
|
settings
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
filteredSettingIds,
|
filteredSettingIds,
|
||||||
|
matchedNavItemKeys,
|
||||||
searchInProgress,
|
searchInProgress,
|
||||||
searchResultsCategories,
|
searchResultsCategories,
|
||||||
queryIsEmpty,
|
queryIsEmpty,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface FormItem {
|
|||||||
|
|
||||||
export interface ISettingGroup {
|
export interface ISettingGroup {
|
||||||
label: string
|
label: string
|
||||||
|
category?: string
|
||||||
settings: SettingParams[]
|
settings: SettingParams[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user