diff --git a/src/platform/settings/components/SettingDialog.vue b/src/platform/settings/components/SettingDialog.vue
index 47fafa87dd..69cbf44fe1 100644
--- a/src/platform/settings/components/SettingDialog.vue
+++ b/src/platform/settings/components/SettingDialog.vue
@@ -44,17 +44,7 @@
-
-
-
-
-
-
-
-
-
+
@@ -64,6 +54,16 @@
+
+
+
+
+
+
+
+
@@ -110,6 +110,7 @@ const {
searchQuery,
inSearch,
searchResultsCategories,
+ matchedNavItemKeys,
handleSearch: handleSearchBase,
getSearchResults
} = useSettingSearch()
@@ -119,16 +120,29 @@ const authActions = useFirebaseAuthActions()
const navRef = ref(null)
const activeCategoryKey = ref(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(() => {
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(() => {
- const category = activeCategoryKey.value
- ? findCategoryByKey(activeCategoryKey.value)
- : null
- return getSearchResults(category)
-})
+const searchResults = computed(() => getSearchResults(null))
// Scroll to and highlight the target setting once the correct category renders.
if (scrollToSettingId) {
diff --git a/src/platform/settings/components/SettingGroup.vue b/src/platform/settings/components/SettingGroup.vue
index deff16f3f9..5824322041 100644
--- a/src/platform/settings/components/SettingGroup.vue
+++ b/src/platform/settings/components/SettingGroup.vue
@@ -2,6 +2,15 @@
+
+ {{
+ $t(
+ `settingsCategories.${normalizeI18nKey(group.category)}`,
+ group.category
+ )
+ }}
+ ›
+
{{
$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
diff --git a/src/platform/settings/composables/useSettingSearch.test.ts b/src/platform/settings/composables/useSettingSearch.test.ts
index 799eebed5c..dd173e8e82 100644
--- a/src/platform/settings/composables/useSettingSearch.test.ts
+++ b/src/platform/settings/composables/useSettingSearch.test.ts
@@ -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 = {}
diff --git a/src/platform/settings/composables/useSettingSearch.ts b/src/platform/settings/composables/useSettingSearch.ts
index a2d29f2a64..a5a6d6fedd 100644
--- a/src/platform/settings/composables/useSettingSearch.ts
+++ b/src/platform/settings/composables/useSettingSearch.ts
@@ -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('')
const filteredSettingIds = ref([])
+ const matchedNavItemKeys = ref>(new Set())
const searchInProgress = ref(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,
diff --git a/src/platform/settings/types.ts b/src/platform/settings/types.ts
index b92c4fe07b..62139cb6fd 100644
--- a/src/platform/settings/types.ts
+++ b/src/platform/settings/types.ts
@@ -62,6 +62,7 @@ export interface FormItem {
export interface ISettingGroup {
label: string
+ category?: string
settings: SettingParams[]
}