Skip to content

Commit b78a856

Browse files
committed
enh(UnifiedSearch): Keep the searchbar on top of the modal
Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent c734a18 commit b78a856

3 files changed

Lines changed: 186 additions & 230 deletions

File tree

core/src/views/UnifiedSearchModal.vue

Lines changed: 134 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -12,78 +12,88 @@
1212
@update:is-open="showDateRangeModal = $event" />
1313
<!-- Unified search form -->
1414
<div ref="unifiedSearch" class="unified-search-modal">
15-
<h1>{{ t('core', 'Unified search') }}</h1>
16-
<NcInputField ref="searchInput"
17-
:value.sync="searchQuery"
18-
type="text"
19-
:label="t('core', 'Search apps, files, tags, messages') + '...'"
20-
@update:value="debouncedFind" />
21-
<div class="unified-search-modal__filters">
22-
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
23-
<template #icon>
24-
<ListBox :size="20" />
25-
</template>
26-
<NcActionButton v-for="provider in providers" :key="provider.id" @click="addProviderFilter(provider)">
15+
<div class="unified-search-modal__header">
16+
<h1>{{ t('core', 'Unified search') }}</h1>
17+
<NcInputField ref="searchInput"
18+
:value.sync="searchQuery"
19+
type="text"
20+
:label="t('core', 'Search apps, files, tags, messages') + '...'"
21+
@update:value="debouncedFind" />
22+
<div class="unified-search-modal__filters">
23+
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
2724
<template #icon>
28-
<img :src="provider.icon">
25+
<ListBox :size="20" />
2926
</template>
30-
{{ t('core', provider.name) }}
31-
</NcActionButton>
32-
</NcActions>
33-
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
34-
<template #icon>
35-
<CalendarRangeIcon :size="20" />
36-
</template>
37-
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
38-
{{ t('core', 'Today') }}
39-
</NcActionButton>
40-
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
41-
{{ t('core', 'Last 7 days') }}
42-
</NcActionButton>
43-
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
44-
{{ t('core', 'Last 30 days') }}
45-
</NcActionButton>
46-
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
47-
{{ t('core', 'This year') }}
48-
</NcActionButton>
49-
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
50-
{{ t('core', 'Last year') }}
51-
</NcActionButton>
52-
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
53-
{{ t('core', 'Custom date range') }}
54-
</NcActionButton>
55-
</NcActions>
56-
<SearchableList :label-text="t('core', 'Search people')"
57-
:search-list="userContacts"
58-
:empty-content-text="t('core', 'Not found')"
59-
@item-selected="applyPersonFilter">
60-
<template #trigger>
61-
<NcButton>
27+
<NcActionButton v-for="provider in providers"
28+
:key="provider.id"
29+
@click="addProviderFilter(provider)">
6230
<template #icon>
63-
<AccountGroup :size="20" />
31+
<img :src="provider.icon" class="filter-button__icon" alt="">
6432
</template>
65-
{{ t('core', 'People') }}
66-
</NcButton>
67-
</template>
68-
</SearchableList>
69-
</div>
70-
<div class="unified-search-modal__filters-applied">
71-
<FilterChip v-for="filter in filters"
72-
:key="filter.id"
73-
:text="filter.name ?? filter.text"
74-
:pretext="''"
75-
@delete="removeFilter(filter)">
76-
<template #icon>
77-
<NcAvatar v-if="filter.type === 'person'"
78-
:user="filter.user"
79-
:size="24"
80-
:disable-menu="true"
81-
:show-user-status="false"
82-
:hide-favorite="false" />
83-
<CalendarRangeIcon v-else-if="filter.type === 'date'" />
84-
<img v-else :src="filter.icon" alt="">
85-
</template>
86-
</FilterChip>
33+
{{ provider.name }}
34+
</NcActionButton>
35+
</NcActions>
36+
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
37+
<template #icon>
38+
<CalendarRangeIcon :size="20" />
39+
</template>
40+
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
41+
{{ t('core', 'Today') }}
42+
</NcActionButton>
43+
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
44+
{{ t('core', 'Last 7 days') }}
45+
</NcActionButton>
46+
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
47+
{{ t('core', 'Last 30 days') }}
48+
</NcActionButton>
49+
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
50+
{{ t('core', 'This year') }}
51+
</NcActionButton>
52+
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
53+
{{ t('core', 'Last year') }}
54+
</NcActionButton>
55+
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
56+
{{ t('core', 'Custom date range') }}
57+
</NcActionButton>
58+
</NcActions>
59+
<SearchableList :label-text="t('core', 'Search people')"
60+
:search-list="userContacts"
61+
:empty-content-text="t('core', 'Not found')"
62+
@item-selected="applyPersonFilter">
63+
<template #trigger>
64+
<NcButton>
65+
<template #icon>
66+
<AccountGroup :size="20" />
67+
</template>
68+
{{ t('core', 'People') }}
69+
</NcButton>
70+
</template>
71+
</SearchableList>
72+
<NcButton v-if="supportFiltering" @click="closeModal">
73+
{{ t('core', 'Filter in current view') }}
74+
<template #icon>
75+
<FilterIcon :size="20" />
76+
</template>
77+
</NcButton>
78+
</div>
79+
<div class="unified-search-modal__filters-applied">
80+
<FilterChip v-for="filter in filters"
81+
:key="filter.id"
82+
:text="filter.name ?? filter.text"
83+
:pretext="''"
84+
@delete="removeFilter(filter)">
85+
<template #icon>
86+
<NcAvatar v-if="filter.type === 'person'"
87+
:user="filter.user"
88+
:size="24"
89+
:disable-menu="true"
90+
:show-user-status="false"
91+
:hide-favorite="false" />
92+
<CalendarRangeIcon v-else-if="filter.type === 'date'" />
93+
<img v-else :src="filter.icon" alt="">
94+
</template>
95+
</FilterChip>
96+
</div>
8797
</div>
8898
<div v-if="noContentInfo.show" class="unified-search-modal__no-content">
8999
<NcEmptyContent :name="noContentInfo.text">
@@ -92,8 +102,8 @@
92102
</template>
93103
</NcEmptyContent>
94104
</div>
95-
<div v-for="providerResult in results" :key="providerResult.id" class="unified-search-modal__results">
96-
<div class="results">
105+
<div class="unified-search-modal__results">
106+
<div v-for="providerResult in results" :key="providerResult.id" class="result">
97107
<div class="result-title">
98108
<span>{{ providerResult.provider }}</span>
99109
</div>
@@ -116,14 +126,6 @@
116126
</div>
117127
</div>
118128
</div>
119-
<div v-if="supportFiltering()" class="unified-search-modal__results">
120-
<NcButton @click="closeModal">
121-
{{ t('core', 'Filter in current view') }}
122-
<template #icon>
123-
<FilterIcon :size="20" />
124-
</template>
125-
</NcButton>
126-
</div>
127129
</div>
128130
</NcModal>
129131
</template>
@@ -150,6 +152,7 @@ import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
150152
151153
import debounce from 'debounce'
152154
import { emit } from '@nextcloud/event-bus'
155+
import { useBrowserLocation } from '@vueuse/core'
153156
import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
154157
155158
export default {
@@ -180,6 +183,15 @@ export default {
180183
required: true,
181184
},
182185
},
186+
setup() {
187+
/**
188+
* Reactive version of window.location
189+
*/
190+
const currentLocation = useBrowserLocation()
191+
return {
192+
currentLocation,
193+
}
194+
},
183195
data() {
184196
return {
185197
providers: [],
@@ -205,22 +217,23 @@ export default {
205217
},
206218
207219
computed: {
208-
userContacts: {
209-
get() {
210-
return this.contacts
211-
},
220+
userContacts() {
221+
return this.contacts
212222
},
213-
noContentInfo: {
214-
get() {
215-
const isEmptySearch = this.searchQuery.length === 0
216-
const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
217-
218-
return {
219-
show: isEmptySearch || hasNoResults,
220-
text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing in search') : t('core', 'No matching results')),
221-
icon: MagnifyIcon,
222-
}
223-
},
223+
noContentInfo() {
224+
const isEmptySearch = this.searchQuery.length === 0
225+
const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
226+
227+
return {
228+
show: isEmptySearch || hasNoResults,
229+
text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing in search') : t('core', 'No matching results')),
230+
icon: MagnifyIcon,
231+
}
232+
},
233+
supportFiltering() {
234+
/* Hard coded apps for the moment this would be improved in coming updates. */
235+
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
236+
return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
224237
},
225238
},
226239
watch: {
@@ -522,21 +535,24 @@ export default {
522535
this.internalIsVisible = false
523536
this.searchQuery = ''
524537
},
525-
supportFiltering() {
526-
/* Hard coded apps for the moment this would be improved in coming updates. */
527-
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
528-
const currentPath = window.location.pathname.replace('/index.php', '')
529-
const containsProvider = providerPaths.some(path => currentPath.includes(path))
530-
return containsProvider
531-
},
532538
},
533539
}
534540
</script>
535541
536542
<style lang="scss" scoped>
537543
.unified-search-modal {
538-
padding: 10px 20px 10px 20px;
539-
height: 60%;
544+
box-sizing: border-box;
545+
height: 100%;
546+
547+
display: flex;
548+
flex-direction: column;
549+
550+
padding-inline: 20px;
551+
padding-block: 10px;
552+
553+
&__header {
554+
padding-block-end: 8px;
555+
}
540556
541557
&__heading {
542558
font-size: 16px;
@@ -547,14 +563,10 @@ export default {
547563
548564
&__filters {
549565
display: flex;
550-
padding-top: 4px;
566+
flex-wrap: wrap;
567+
gap: 4px;
551568
justify-content: left;
552-
553-
>* {
554-
margin-right: 4px;
555-
556-
}
557-
569+
padding-top: 4px;
558570
}
559571
560572
&__filters-applied {
@@ -570,19 +582,18 @@ export default {
570582
}
571583
572584
&__results {
573-
padding: 10px;
585+
overflow: hidden scroll;
574586
575-
.results {
576-
577-
.result-title {
587+
.result {
588+
&-title {
578589
span {
579590
color: var(--color-primary-element);
580591
font-weight: bolder;
581592
font-size: 16px;
582593
}
583594
}
584595
585-
.result-footer {
596+
&-footer {
586597
justify-content: space-between;
587598
align-items: center;
588599
display: flex;
@@ -592,20 +603,18 @@ export default {
592603
}
593604
}
594605
595-
div.v-popper__wrapper {
596-
ul {
597-
li {
598-
::v-deep button.action-button {
599-
align-items: center !important;
600-
601-
img {
606+
.filter-button__icon {
607+
height: 20px;
602608
width: 20px;
603-
margin: 0 4px;
609+
object-fit: contain;
604610
filter: var(--background-invert-if-bright);
605-
}
611+
padding: 11px; // align with text to fit at least 44px
612+
}
606613
607-
}
608-
}
614+
// Ensure modal is accessible on small devices
615+
@media only screen and (max-height: 400px) {
616+
.unified-search-modal__results {
617+
overflow: unset;
609618
}
610619
}
611620
</style>

0 commit comments

Comments
 (0)