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:
Chenlei Hu
2024-08-10 17:26:57 -04:00
committed by GitHub
parent 3e7b0a4907
commit 7ce7490bc3
5 changed files with 194 additions and 6 deletions

View 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>

View File

@@ -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;

View File

@@ -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()

View 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>

View File

@@ -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: '队列',