Skip to content

Commit 7ce7490

Browse files
authored
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
1 parent 3e7b0a4 commit 7ce7490

File tree

5 files changed

+194
-6
lines changed

5 files changed

+194
-6
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div class="no-results-placeholder">
3+
<Card>
4+
<template #content>
5+
<div class="flex flex-column align-items-center">
6+
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem"></i>
7+
<h3>{{ title }}</h3>
8+
<p>{{ message }}</p>
9+
<Button
10+
v-if="buttonLabel"
11+
:label="buttonLabel"
12+
@click="$emit('action')"
13+
class="p-button-text"
14+
/>
15+
</div>
16+
</template>
17+
</Card>
18+
</div>
19+
</template>
20+
21+
<script setup lang="ts">
22+
import Card from 'primevue/card'
23+
import Button from 'primevue/button'
24+
25+
defineProps<{
26+
icon?: string
27+
title: string
28+
message: string
29+
buttonLabel?: string
30+
}>()
31+
32+
defineEmits(['action'])
33+
</script>
34+
35+
<style scoped>
36+
.no-results-placeholder {
37+
display: flex;
38+
justify-content: center;
39+
align-items: center;
40+
height: 100%;
41+
padding: 2rem;
42+
}
43+
44+
.no-results-placeholder :deep(.p-card) {
45+
background-color: var(--surface-ground);
46+
text-align: center;
47+
}
48+
49+
.no-results-placeholder h3 {
50+
color: var(--text-color);
51+
margin-bottom: 0.5rem;
52+
}
53+
54+
.no-results-placeholder p {
55+
color: var(--text-color-secondary);
56+
margin-bottom: 1rem;
57+
}
58+
</style>

src/components/dialog/content/SettingDialogContent.vue

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,49 @@
11
<template>
22
<div class="settings-container">
33
<div class="settings-sidebar">
4+
<SettingSearchBox
5+
class="settings-search-box"
6+
v-model:modelValue="searchQuery"
7+
@search="handleSearch"
8+
/>
49
<Listbox
510
v-model="activeCategory"
611
:options="categories"
712
optionLabel="label"
813
scrollHeight="100%"
14+
:disabled="inSearch"
915
:pt="{ root: { class: 'border-none' } }"
1016
/>
1117
</div>
1218
<Divider layout="vertical" />
13-
<div class="settings-content" v-if="activeCategory">
14-
<Tabs :value="activeCategory.label">
15-
<TabPanels>
19+
<div class="settings-content">
20+
<Tabs :value="tabValue">
21+
<TabPanels class="settings-tab-panels">
22+
<TabPanel key="search-results" value="Search Results">
23+
<div v-if="searchResults.length > 0">
24+
<SettingGroup
25+
v-for="(group, i) in searchResults"
26+
:key="group.label"
27+
:divider="i !== 0"
28+
:group="group"
29+
/>
30+
</div>
31+
<NoResultsPlaceholder
32+
v-else
33+
icon="pi pi-search"
34+
:title="$t('noResultsFound')"
35+
:message="$t('searchFailedMessage')"
36+
/>
37+
</TabPanel>
1638
<TabPanel
1739
v-for="category in categories"
1840
:key="category.key"
1941
:value="category.label"
2042
>
2143
<SettingGroup
22-
v-for="group in sortedGroups(category)"
44+
v-for="(group, i) in sortedGroups(category)"
2345
:key="group.label"
46+
:divider="i !== 0"
2447
:group="{
2548
label: group.label,
2649
settings: flattenTree<SettingParams>(group)
@@ -43,15 +66,22 @@ import Divider from 'primevue/divider'
4366
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
4467
import { SettingParams } from '@/types/settingTypes'
4568
import SettingGroup from './setting/SettingGroup.vue'
69+
import SettingSearchBox from './setting/SettingSearchBox.vue'
70+
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
4671
import { flattenTree } from '@/utils/treeUtil'
4772
73+
interface ISettingGroup {
74+
label: string
75+
settings: SettingParams[]
76+
}
77+
4878
const settingStore = useSettingStore()
4979
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
5080
const categories = computed<SettingTreeNode[]>(
5181
() => settingRoot.value.children || []
5282
)
53-
5483
const activeCategory = ref<SettingTreeNode | null>(null)
84+
const searchResults = ref<ISettingGroup[]>([])
5585
5686
watch(activeCategory, (newCategory, oldCategory) => {
5787
if (newCategory === null) {
@@ -68,13 +98,59 @@ const sortedGroups = (category: SettingTreeNode) => {
6898
a.label.localeCompare(b.label)
6999
)
70100
}
101+
102+
const searchQuery = ref<string>('')
103+
const searchInProgress = ref<boolean>(false)
104+
watch(searchQuery, () => (searchInProgress.value = true))
105+
106+
const handleSearch = (query: string) => {
107+
if (!query) {
108+
searchResults.value = []
109+
return
110+
}
111+
112+
const allSettings = flattenTree<SettingParams>(settingRoot.value)
113+
const filteredSettings = allSettings.filter(
114+
(setting) =>
115+
setting.id.toLowerCase().includes(query.toLowerCase()) ||
116+
setting.name.toLowerCase().includes(query.toLowerCase())
117+
)
118+
119+
const groupedSettings: { [key: string]: SettingParams[] } = {}
120+
filteredSettings.forEach((setting) => {
121+
const groupLabel = setting.id.split('.')[1]
122+
if (!groupedSettings[groupLabel]) {
123+
groupedSettings[groupLabel] = []
124+
}
125+
groupedSettings[groupLabel].push(setting)
126+
})
127+
128+
searchResults.value = Object.entries(groupedSettings).map(
129+
([label, settings]) => ({
130+
label,
131+
settings
132+
})
133+
)
134+
searchInProgress.value = false
135+
}
136+
137+
const inSearch = computed(
138+
() => searchQuery.value.length > 0 && !searchInProgress.value
139+
)
140+
const tabValue = computed(() =>
141+
inSearch.value ? 'Search Results' : activeCategory.value?.label
142+
)
71143
</script>
72144

73145
<style>
74146
/* Remove after we have tailwind setup */
75147
.border-none {
76148
border: none !important;
77149
}
150+
151+
.settings-tab-panels {
152+
padding-top: 0px !important;
153+
}
78154
</style>
79155

80156
<style scoped>
@@ -95,6 +171,11 @@ const sortedGroups = (category: SettingTreeNode) => {
95171
padding: 10px;
96172
}
97173
174+
.settings-search-box {
175+
width: 100%;
176+
margin-bottom: 10px;
177+
}
178+
98179
.settings-content {
99180
flex-grow: 1;
100181
overflow-y: auto;

src/components/dialog/content/setting/SettingGroup.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="setting-group">
3-
<Divider />
3+
<Divider v-if="divider" />
44
<h3>{{ group.label }}</h3>
55
<div
66
v-for="setting in group.settings"
@@ -46,6 +46,7 @@ defineProps<{
4646
label: string
4747
settings: SettingParams[]
4848
}
49+
divider?: boolean
4950
}>()
5051
5152
const settingStore = useSettingStore()
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<template>
2+
<IconField :class="props.class">
3+
<InputIcon class="pi pi-search" />
4+
<InputText
5+
class="search-box-input"
6+
@input="handleInput"
7+
:modelValue="props.modelValue"
8+
:placeholder="$t('searchSettings') + '...'"
9+
/>
10+
</IconField>
11+
</template>
12+
13+
<script setup lang="ts">
14+
import IconField from 'primevue/iconfield'
15+
import InputIcon from 'primevue/inputicon'
16+
import InputText from 'primevue/inputtext'
17+
import { debounce } from 'lodash'
18+
19+
const props = defineProps<{
20+
class?: string
21+
modelValue: string
22+
}>()
23+
const emit = defineEmits(['update:modelValue', 'search'])
24+
const emitSearch = debounce((event: KeyboardEvent) => {
25+
const target = event.target as HTMLInputElement
26+
emit('search', target.value)
27+
}, 300)
28+
29+
const handleInput = (event: KeyboardEvent) => {
30+
const target = event.target as HTMLInputElement
31+
emit('update:modelValue', target.value)
32+
emitSearch(event)
33+
}
34+
</script>
35+
36+
<style scoped>
37+
.search-box-input {
38+
width: 100%;
39+
}
40+
</style>

src/i18n.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { createI18n } from 'vue-i18n'
33
const messages = {
44
en: {
55
settings: 'Settings',
6+
searchSettings: 'Search Settings',
7+
noResultsFound: 'No Results Found',
8+
searchFailedMessage:
9+
"We couldn't find any settings matching your search. Try adjusting your search terms.",
610
sideToolBar: {
711
themeToggle: 'Toggle Theme',
812
queue: 'Queue',
@@ -14,6 +18,10 @@ const messages = {
1418
},
1519
zh: {
1620
settings: '设置',
21+
searchSettings: '搜索设置',
22+
noResultsFound: '未找到结果',
23+
searchFailedMessage:
24+
'我们找不到与您的搜索匹配的任何设置。请尝试调整搜索条件。',
1725
sideToolBar: {
1826
themeToggle: '主题切换',
1927
queue: '队列',

0 commit comments

Comments
 (0)