mirror of
https://github.com/nextcloud/server.git
synced 2026-02-23 09:53:17 -05:00
enh(UnifiedSearch): Keep the searchbar on top of the modal
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
e49c331b71
commit
ffeecd86cf
1 changed files with 140 additions and 128 deletions
|
|
@ -10,79 +10,89 @@
|
|||
@update:is-open="showDateRangeModal = $event" />
|
||||
<!-- Unified search form -->
|
||||
<div ref="unifiedSearch" class="unified-search-modal">
|
||||
<h2>{{ t('core', 'Unified search') }}</h2>
|
||||
<NcInputField ref="searchInput"
|
||||
:value.sync="searchQuery"
|
||||
type="text"
|
||||
:label="t('core', 'Search apps, files, tags, messages') + '...'"
|
||||
@update:value="debouncedFind" />
|
||||
<div class="unified-search-modal__filters">
|
||||
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
|
||||
<template #icon>
|
||||
<ListBox :size="20" />
|
||||
</template>
|
||||
<NcActionButton v-for="provider in providers" :key="provider.id" @click="addProviderFilter(provider)">
|
||||
<div class="unified-search-modal__header">
|
||||
<h2>{{ t('core', 'Unified search') }}</h2>
|
||||
<NcInputField ref="searchInput"
|
||||
:value.sync="searchQuery"
|
||||
type="text"
|
||||
:label="t('core', 'Search apps, files, tags, messages') + '...'"
|
||||
@update:value="debouncedFind" />
|
||||
<div class="unified-search-modal__filters">
|
||||
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
|
||||
<template #icon>
|
||||
<img :src="provider.icon">
|
||||
<ListBox :size="20" />
|
||||
</template>
|
||||
{{ t('core', provider.name) }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
|
||||
<template #icon>
|
||||
<CalendarRangeIcon :size="20" />
|
||||
</template>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
|
||||
{{ t('core', 'Today') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
|
||||
{{ t('core', 'Last 7 days') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
|
||||
{{ t('core', 'Last 30 days') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
|
||||
{{ t('core', 'This year') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
|
||||
{{ t('core', 'Last year') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
|
||||
{{ t('core', 'Custom date range') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<SearchableList :label-text="t('core', 'Search people')"
|
||||
:search-list="userContacts"
|
||||
:empty-content-text="t('core', 'Not found')"
|
||||
@search-term-change="debouncedFilterContacts"
|
||||
@item-selected="applyPersonFilter">
|
||||
<template #trigger>
|
||||
<NcButton>
|
||||
<NcActionButton v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
@click="addProviderFilter(provider)">
|
||||
<template #icon>
|
||||
<AccountGroup :size="20" />
|
||||
<img :src="provider.icon" class="filter-button__icon" alt="">
|
||||
</template>
|
||||
{{ t('core', 'People') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</SearchableList>
|
||||
</div>
|
||||
<div class="unified-search-modal__filters-applied">
|
||||
<FilterChip v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:text="filter.name ?? filter.text"
|
||||
:pretext="''"
|
||||
@delete="removeFilter(filter)">
|
||||
<template #icon>
|
||||
<NcAvatar v-if="filter.type === 'person'"
|
||||
:user="filter.user"
|
||||
:size="24"
|
||||
:disable-menu="true"
|
||||
:show-user-status="false"
|
||||
:hide-favorite="false" />
|
||||
<CalendarRangeIcon v-else-if="filter.type === 'date'" />
|
||||
<img v-else :src="filter.icon" alt="">
|
||||
</template>
|
||||
</FilterChip>
|
||||
{{ provider.name }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
|
||||
<template #icon>
|
||||
<CalendarRangeIcon :size="20" />
|
||||
</template>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
|
||||
{{ t('core', 'Today') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
|
||||
{{ t('core', 'Last 7 days') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
|
||||
{{ t('core', 'Last 30 days') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
|
||||
{{ t('core', 'This year') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
|
||||
{{ t('core', 'Last year') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
|
||||
{{ t('core', 'Custom date range') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<SearchableList :label-text="t('core', 'Search people')"
|
||||
:search-list="userContacts"
|
||||
:empty-content-text="t('core', 'Not found')"
|
||||
@search-term-change="debouncedFilterContacts"
|
||||
@item-selected="applyPersonFilter">
|
||||
<template #trigger>
|
||||
<NcButton>
|
||||
<template #icon>
|
||||
<AccountGroup :size="20" />
|
||||
</template>
|
||||
{{ t('core', 'People') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</SearchableList>
|
||||
<NcButton v-if="supportFiltering" @click="closeModal">
|
||||
{{ t('core', 'Filter in current view') }}
|
||||
<template #icon>
|
||||
<FilterIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="unified-search-modal__filters-applied">
|
||||
<FilterChip v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:text="filter.name ?? filter.text"
|
||||
:pretext="''"
|
||||
@delete="removeFilter(filter)">
|
||||
<template #icon>
|
||||
<NcAvatar v-if="filter.type === 'person'"
|
||||
:user="filter.user"
|
||||
:size="24"
|
||||
:disable-menu="true"
|
||||
:show-user-status="false"
|
||||
:hide-favorite="false" />
|
||||
<CalendarRangeIcon v-else-if="filter.type === 'date'" />
|
||||
<img v-else :src="filter.icon" alt="">
|
||||
</template>
|
||||
</FilterChip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="noContentInfo.show" class="unified-search-modal__no-content">
|
||||
<NcEmptyContent :name="noContentInfo.text">
|
||||
|
|
@ -91,8 +101,8 @@
|
|||
</template>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
<div v-for="providerResult in results" :key="providerResult.id" class="unified-search-modal__results">
|
||||
<div class="results">
|
||||
<div v-else class="unified-search-modal__results">
|
||||
<div v-for="providerResult in results" :key="providerResult.id" class="result">
|
||||
<div class="result-title">
|
||||
<span>{{ providerResult.provider }}</span>
|
||||
</div>
|
||||
|
|
@ -115,14 +125,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="supportFiltering()" class="unified-search-modal__results">
|
||||
<NcButton @click="closeModal">
|
||||
{{ t('core', 'Filter in current view') }}
|
||||
<template #icon>
|
||||
<FilterIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
|
@ -149,6 +151,7 @@ import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
|
|||
|
||||
import debounce from 'debounce'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { useBrowserLocation } from '@vueuse/core'
|
||||
import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
|
||||
|
||||
export default {
|
||||
|
|
@ -179,6 +182,15 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
/**
|
||||
* Reactive version of window.location
|
||||
*/
|
||||
const currentLocation = useBrowserLocation()
|
||||
return {
|
||||
currentLocation,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
providers: [],
|
||||
|
|
@ -205,22 +217,22 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
userContacts: {
|
||||
get() {
|
||||
return this.contacts
|
||||
},
|
||||
userContacts() {
|
||||
return this.contacts
|
||||
},
|
||||
noContentInfo: {
|
||||
get() {
|
||||
const isEmptySearch = this.searchQuery.length === 0
|
||||
const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
|
||||
|
||||
return {
|
||||
show: isEmptySearch || hasNoResults,
|
||||
text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing to search') : t('core', 'No matching results')),
|
||||
icon: MagnifyIcon,
|
||||
}
|
||||
},
|
||||
noContentInfo() {
|
||||
const isEmptySearch = this.searchQuery.length === 0
|
||||
const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
|
||||
return {
|
||||
show: isEmptySearch || hasNoResults,
|
||||
text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing to search') : t('core', 'No matching results')),
|
||||
icon: MagnifyIcon,
|
||||
}
|
||||
},
|
||||
supportFiltering() {
|
||||
/* Hard coded apps for the moment this would be improved in coming updates. */
|
||||
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
|
||||
return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -522,21 +534,27 @@ export default {
|
|||
this.internalIsVisible = false
|
||||
this.searchQuery = ''
|
||||
},
|
||||
supportFiltering() {
|
||||
/* Hard coded apps for the moment this would be improved in coming updates. */
|
||||
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
|
||||
const currentPath = window.location.pathname.replace('/index.php', '')
|
||||
const containsProvider = providerPaths.some(path => currentPath.includes(path))
|
||||
return containsProvider
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.unified-search-modal {
|
||||
padding: 10px 20px 10px 20px;
|
||||
height: 60%;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-block: 10px 0;
|
||||
|
||||
// inline padding on direct children to make sure the scrollbar is on the modal container
|
||||
> * {
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding-block-end: 8px;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
font-size: 16px;
|
||||
|
|
@ -547,14 +565,10 @@ export default {
|
|||
|
||||
&__filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: start;
|
||||
padding-top: 4px;
|
||||
justify-content: left;
|
||||
|
||||
>* {
|
||||
margin-right: 4px;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__filters-applied {
|
||||
|
|
@ -570,11 +584,11 @@ export default {
|
|||
}
|
||||
|
||||
&__results {
|
||||
padding: 10px;
|
||||
overflow: hidden scroll;
|
||||
padding-block: 0 10px;
|
||||
|
||||
.results {
|
||||
|
||||
.result-title {
|
||||
.result {
|
||||
&-title {
|
||||
span {
|
||||
color: var(--color-primary-element);
|
||||
font-weight: bolder;
|
||||
|
|
@ -582,7 +596,7 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.result-footer {
|
||||
&-footer {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
|
@ -592,20 +606,18 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
div.v-popper__wrapper {
|
||||
ul {
|
||||
li {
|
||||
::v-deep button.action-button {
|
||||
align-items: center !important;
|
||||
.filter-button__icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
object-fit: contain;
|
||||
filter: var(--background-invert-if-bright);
|
||||
padding: 11px; // align with text to fit at least 44px
|
||||
}
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
margin: 0 4px;
|
||||
filter: var(--background-invert-if-bright);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// Ensure modal is accessible on small devices
|
||||
@media only screen and (max-height: 400px) {
|
||||
.unified-search-modal__results {
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue