Allow unified search filtering
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
34aca46325
commit
f359529555
|
@ -0,0 +1,77 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<a :title="t('core', 'Search for {name} only', { name })"
|
||||||
|
class="unified-search__filter"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="onClick">
|
||||||
|
{{ filter }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SearchFilter',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filter() {
|
||||||
|
return `in:${this.type}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.$emit('click', this.filter)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.unified-search__filter {
|
||||||
|
height: 1em;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 1em;
|
||||||
|
background-color: var(--color-background-darker);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,3 +1,24 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<a :href="resourceUrl || '#'"
|
<a :href="resourceUrl || '#'"
|
||||||
class="unified-search__result"
|
class="unified-search__result"
|
||||||
|
|
|
@ -25,6 +25,9 @@ import axios from '@nextcloud/axios'
|
||||||
|
|
||||||
export const defaultLimit = loadState('unified-search', 'limit-default')
|
export const defaultLimit = loadState('unified-search', 'limit-default')
|
||||||
export const minSearchLength = 2
|
export const minSearchLength = 2
|
||||||
|
export const regexFilterIn = /[^-]in:([a-z_-]+)/ig
|
||||||
|
export const regexFilterNot = /-in:([a-z_-]+)/ig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of available search providers
|
* Get the list of available search providers
|
||||||
*
|
*
|
||||||
|
|
|
@ -41,6 +41,17 @@
|
||||||
@keypress.enter.prevent.stop="onInputEnter">
|
@keypress.enter.prevent.stop="onInputEnter">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search filters -->
|
||||||
|
<div v-if="availableFilters.length > 1" class="unified-search__filters">
|
||||||
|
<ul>
|
||||||
|
<SearchFilter v-for="type in availableFilters"
|
||||||
|
:key="type"
|
||||||
|
:type="type"
|
||||||
|
:name="typesMap[type]"
|
||||||
|
@click="onClickFilter" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template v-if="!hasResults">
|
<template v-if="!hasResults">
|
||||||
<!-- Loading placeholders -->
|
<!-- Loading placeholders -->
|
||||||
<ul v-if="isLoading">
|
<ul v-if="isLoading">
|
||||||
|
@ -97,13 +108,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { minSearchLength, getTypes, search, defaultLimit } from '../services/UnifiedSearchService'
|
import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot } from '../services/UnifiedSearchService'
|
||||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||||
import Magnify from 'vue-material-design-icons/Magnify'
|
import Magnify from 'vue-material-design-icons/Magnify'
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
import { emit } from '@nextcloud/event-bus'
|
import { emit } from '@nextcloud/event-bus'
|
||||||
|
|
||||||
import HeaderMenu from '../components/HeaderMenu'
|
import HeaderMenu from '../components/HeaderMenu'
|
||||||
|
import SearchFilter from '../components/UnifiedSearch/SearchFilter'
|
||||||
import SearchResult from '../components/UnifiedSearch/SearchResult'
|
import SearchResult from '../components/UnifiedSearch/SearchResult'
|
||||||
import SearchResultPlaceholder from '../components/UnifiedSearch/SearchResultPlaceholder'
|
import SearchResultPlaceholder from '../components/UnifiedSearch/SearchResultPlaceholder'
|
||||||
|
|
||||||
|
@ -114,6 +126,7 @@ export default {
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
HeaderMenu,
|
HeaderMenu,
|
||||||
Magnify,
|
Magnify,
|
||||||
|
SearchFilter,
|
||||||
SearchResult,
|
SearchResult,
|
||||||
SearchResultPlaceholder,
|
SearchResultPlaceholder,
|
||||||
},
|
},
|
||||||
|
@ -162,10 +175,10 @@ export default {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return ordered results
|
* Return ordered results
|
||||||
* @returns {Object}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
orderedResults() {
|
orderedResults() {
|
||||||
return Object.values(this.typesIDs)
|
return this.typesIDs
|
||||||
.filter(type => type in this.results)
|
.filter(type => type in this.results)
|
||||||
.map(type => ({
|
.map(type => ({
|
||||||
type,
|
type,
|
||||||
|
@ -173,6 +186,41 @@ export default {
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available filters
|
||||||
|
* We only show filters that are available on the results
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
availableFilters() {
|
||||||
|
return Object.keys(this.results)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applied filters
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
usedFiltersIn() {
|
||||||
|
let match
|
||||||
|
const filters = []
|
||||||
|
while ((match = regexFilterIn.exec(this.query)) !== null) {
|
||||||
|
filters.push(match[1])
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applied anti filters
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
usedFiltersNot() {
|
||||||
|
let match
|
||||||
|
const filters = []
|
||||||
|
while ((match = regexFilterNot.exec(this.query)) !== null) {
|
||||||
|
filters.push(match[1])
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the current search too short
|
* Is the current search too short
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
@ -291,12 +339,30 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let types = this.typesIDs
|
||||||
|
let query = this.query
|
||||||
|
|
||||||
|
// Filter out types
|
||||||
|
if (this.usedFiltersNot.length > 0) {
|
||||||
|
types = this.typesIDs.filter(type => this.usedFiltersNot.indexOf(type) === -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only use those filters if any and check if they are valid
|
||||||
|
if (this.usedFiltersIn.length > 0) {
|
||||||
|
types = this.typesIDs.filter(type => this.usedFiltersIn.indexOf(type) > -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any filters from the query
|
||||||
|
query = query.replace(regexFilterIn, '').replace(regexFilterNot, '')
|
||||||
|
|
||||||
|
console.debug('Searching', query, 'in', types)
|
||||||
|
|
||||||
// reset search if the query changed
|
// reset search if the query changed
|
||||||
this.resetState()
|
this.resetState()
|
||||||
|
|
||||||
this.typesIDs.forEach(async type => {
|
types.forEach(async type => {
|
||||||
this.$set(this.loading, type, true)
|
this.$set(this.loading, type, true)
|
||||||
const request = await search(type, this.query)
|
const request = await search(type, query)
|
||||||
|
|
||||||
// Process results
|
// Process results
|
||||||
if (request.data.entries.length > 0) {
|
if (request.data.entries.length > 0) {
|
||||||
|
@ -478,6 +544,13 @@ export default {
|
||||||
this.focused = index
|
this.focused = index
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClickFilter(filter) {
|
||||||
|
this.query = `${this.query} ${filter}`
|
||||||
|
.replace(/ {2}/g, ' ')
|
||||||
|
.trim()
|
||||||
|
this.onInput()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -500,6 +573,14 @@ $input-padding: 6px;
|
||||||
background-color: var(--color-main-background);
|
background-color: var(--color-main-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__filters {
|
||||||
|
margin: $margin / 2 $margin;
|
||||||
|
ul {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
// Minus margins
|
// Minus margins
|
||||||
width: calc(100% - 2 * #{$margin});
|
width: calc(100% - 2 * #{$margin});
|
||||||
|
@ -510,10 +591,9 @@ $input-padding: 6px;
|
||||||
&[placeholder],
|
&[placeholder],
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow:ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__results {
|
&__results {
|
||||||
|
|
Loading…
Reference in New Issue