mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 00:50:05 +00:00
Add search settings feature (#362)
* Add setting searchbox ui * Basic search * Remove first divider * Keep group label on search result * No result placeholder * Prevent no result flash * i18n * Disable category nav when searching
This commit is contained in:
58
src/components/common/NoResultsPlaceholder.vue
Normal file
58
src/components/common/NoResultsPlaceholder.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="no-results-placeholder">
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex flex-column align-items-center">
|
||||
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem"></i>
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ message }}</p>
|
||||
<Button
|
||||
v-if="buttonLabel"
|
||||
:label="buttonLabel"
|
||||
@click="$emit('action')"
|
||||
class="p-button-text"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Card from 'primevue/card'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
defineProps<{
|
||||
icon?: string
|
||||
title: string
|
||||
message: string
|
||||
buttonLabel?: string
|
||||
}>()
|
||||
|
||||
defineEmits(['action'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-results-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.no-results-placeholder :deep(.p-card) {
|
||||
background-color: var(--surface-ground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-results-placeholder h3 {
|
||||
color: var(--text-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.no-results-placeholder p {
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,49 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="settings-sidebar">
|
||||
<SettingSearchBox
|
||||
class="settings-search-box"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<Listbox
|
||||
v-model="activeCategory"
|
||||
:options="categories"
|
||||
optionLabel="label"
|
||||
scrollHeight="100%"
|
||||
:disabled="inSearch"
|
||||
:pt="{ root: { class: 'border-none' } }"
|
||||
/>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<div class="settings-content" v-if="activeCategory">
|
||||
<Tabs :value="activeCategory.label">
|
||||
<TabPanels>
|
||||
<div class="settings-content">
|
||||
<Tabs :value="tabValue">
|
||||
<TabPanels class="settings-tab-panels">
|
||||
<TabPanel key="search-results" value="Search Results">
|
||||
<div v-if="searchResults.length > 0">
|
||||
<SettingGroup
|
||||
v-for="(group, i) in searchResults"
|
||||
:key="group.label"
|
||||
:divider="i !== 0"
|
||||
:group="group"
|
||||
/>
|
||||
</div>
|
||||
<NoResultsPlaceholder
|
||||
v-else
|
||||
icon="pi pi-search"
|
||||
:title="$t('noResultsFound')"
|
||||
:message="$t('searchFailedMessage')"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel
|
||||
v-for="category in categories"
|
||||
:key="category.key"
|
||||
:value="category.label"
|
||||
>
|
||||
<SettingGroup
|
||||
v-for="group in sortedGroups(category)"
|
||||
v-for="(group, i) in sortedGroups(category)"
|
||||
:key="group.label"
|
||||
:divider="i !== 0"
|
||||
:group="{
|
||||
label: group.label,
|
||||
settings: flattenTree<SettingParams>(group)
|
||||
@@ -43,15 +66,22 @@ import Divider from 'primevue/divider'
|
||||
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import SettingGroup from './setting/SettingGroup.vue'
|
||||
import SettingSearchBox from './setting/SettingSearchBox.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
||||
const categories = computed<SettingTreeNode[]>(
|
||||
() => settingRoot.value.children || []
|
||||
)
|
||||
|
||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||
const searchResults = ref<ISettingGroup[]>([])
|
||||
|
||||
watch(activeCategory, (newCategory, oldCategory) => {
|
||||
if (newCategory === null) {
|
||||
@@ -68,6 +98,48 @@ const sortedGroups = (category: SettingTreeNode) => {
|
||||
a.label.localeCompare(b.label)
|
||||
)
|
||||
}
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
const searchInProgress = ref<boolean>(false)
|
||||
watch(searchQuery, () => (searchInProgress.value = true))
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
if (!query) {
|
||||
searchResults.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const allSettings = flattenTree<SettingParams>(settingRoot.value)
|
||||
const filteredSettings = allSettings.filter(
|
||||
(setting) =>
|
||||
setting.id.toLowerCase().includes(query.toLowerCase()) ||
|
||||
setting.name.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
|
||||
const groupedSettings: { [key: string]: SettingParams[] } = {}
|
||||
filteredSettings.forEach((setting) => {
|
||||
const groupLabel = setting.id.split('.')[1]
|
||||
if (!groupedSettings[groupLabel]) {
|
||||
groupedSettings[groupLabel] = []
|
||||
}
|
||||
groupedSettings[groupLabel].push(setting)
|
||||
})
|
||||
|
||||
searchResults.value = Object.entries(groupedSettings).map(
|
||||
([label, settings]) => ({
|
||||
label,
|
||||
settings
|
||||
})
|
||||
)
|
||||
searchInProgress.value = false
|
||||
}
|
||||
|
||||
const inSearch = computed(
|
||||
() => searchQuery.value.length > 0 && !searchInProgress.value
|
||||
)
|
||||
const tabValue = computed(() =>
|
||||
inSearch.value ? 'Search Results' : activeCategory.value?.label
|
||||
)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -75,6 +147,10 @@ const sortedGroups = (category: SettingTreeNode) => {
|
||||
.border-none {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.settings-tab-panels {
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
@@ -95,6 +171,11 @@ const sortedGroups = (category: SettingTreeNode) => {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.settings-search-box {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="setting-group">
|
||||
<Divider />
|
||||
<Divider v-if="divider" />
|
||||
<h3>{{ group.label }}</h3>
|
||||
<div
|
||||
v-for="setting in group.settings"
|
||||
@@ -46,6 +46,7 @@ defineProps<{
|
||||
label: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
divider?: boolean
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
40
src/components/dialog/content/setting/SettingSearchBox.vue
Normal file
40
src/components/dialog/content/setting/SettingSearchBox.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<IconField :class="props.class">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
class="search-box-input"
|
||||
@input="handleInput"
|
||||
:modelValue="props.modelValue"
|
||||
:placeholder="$t('searchSettings') + '...'"
|
||||
/>
|
||||
</IconField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: string
|
||||
modelValue: string
|
||||
}>()
|
||||
const emit = defineEmits(['update:modelValue', 'search'])
|
||||
const emitSearch = debounce((event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('search', target.value)
|
||||
}, 300)
|
||||
|
||||
const handleInput = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
emitSearch(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-box-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,10 @@ import { createI18n } from 'vue-i18n'
|
||||
const messages = {
|
||||
en: {
|
||||
settings: 'Settings',
|
||||
searchSettings: 'Search Settings',
|
||||
noResultsFound: 'No Results Found',
|
||||
searchFailedMessage:
|
||||
"We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
sideToolBar: {
|
||||
themeToggle: 'Toggle Theme',
|
||||
queue: 'Queue',
|
||||
@@ -14,6 +18,10 @@ const messages = {
|
||||
},
|
||||
zh: {
|
||||
settings: '设置',
|
||||
searchSettings: '搜索设置',
|
||||
noResultsFound: '未找到结果',
|
||||
searchFailedMessage:
|
||||
'我们找不到与您的搜索匹配的任何设置。请尝试调整搜索条件。',
|
||||
sideToolBar: {
|
||||
themeToggle: '主题切换',
|
||||
queue: '队列',
|
||||
|
||||
Reference in New Issue
Block a user