|
|
|
@ -11,79 +11,89 @@
|
|
|
|
|
@update:is-open="showDateRangeModal = $event" />
|
|
|
|
|
<!-- Unified search form -->
|
|
|
|
|
<div ref="unifiedSearch" class="unified-search-modal">
|
|
|
|
|
<h1>{{ t('core', 'Unified search') }}</h1>
|
|
|
|
|
<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">
|
|
|
|
|
<h1>{{ t('core', 'Unified search') }}</h1>
|
|
|
|
|
<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">
|
|
|
|
@ -92,8 +102,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>
|
|
|
|
@ -116,14 +126,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>
|
|
|
|
@ -150,6 +152,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 {
|
|
|
|
@ -180,6 +183,15 @@ export default {
|
|
|
|
|
required: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
setup() {
|
|
|
|
|
/**
|
|
|
|
|
* Reactive version of window.location
|
|
|
|
|
*/
|
|
|
|
|
const currentLocation = useBrowserLocation()
|
|
|
|
|
return {
|
|
|
|
|
currentLocation,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
providers: [],
|
|
|
|
@ -206,22 +218,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: {
|
|
|
|
@ -523,21 +535,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;
|
|
|
|
@ -548,14 +566,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 {
|
|
|
|
@ -571,11 +585,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;
|
|
|
|
@ -583,7 +597,7 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-footer {
|
|
|
|
|
&-footer {
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
display: flex;
|
|
|
|
@ -593,20 +607,18 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
div.v-popper__wrapper {
|
|
|
|
|
ul {
|
|
|
|
|
li {
|
|
|
|
|
::v-deep button.action-button {
|
|
|
|
|
align-items: center !important;
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
width: 20px;
|
|
|
|
|
margin: 0 4px;
|
|
|
|
|
filter: var(--background-invert-if-bright);
|
|
|
|
|
}
|
|
|
|
|
.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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Ensure modal is accessible on small devices
|
|
|
|
|
@media only screen and (max-height: 400px) {
|
|
|
|
|
.unified-search-modal__results {
|
|
|
|
|
overflow: unset;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|