]> source.dussan.org Git - nextcloud-server.git/commitdiff
Allow unified search filtering
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Wed, 5 Aug 2020 10:56:11 +0000 (12:56 +0200)
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Thu, 3 Sep 2020 07:03:09 +0000 (09:03 +0200)
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
core/src/components/UnifiedSearch/SearchFilter.vue [new file with mode: 0644]
core/src/components/UnifiedSearch/SearchResult.vue
core/src/services/UnifiedSearchService.js
core/src/views/UnifiedSearch.vue

diff --git a/core/src/components/UnifiedSearch/SearchFilter.vue b/core/src/components/UnifiedSearch/SearchFilter.vue
new file mode 100644 (file)
index 0000000..f3c06a2
--- /dev/null
@@ -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>
index d5374832da950f7619bc2a0e0eb663cf70553297..025ddef86c06f80b18e2e7abe72689b00fb91e46 100644 (file)
@@ -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>
        <a :href="resourceUrl || '#'"
                class="unified-search__result"
index 1507ad5a2bd1dc1f2ffc689af6ed6252632eb133..152de3755f7c75f74252ffd0f2bb4fa51d3b5af9 100644 (file)
@@ -25,6 +25,9 @@ import axios from '@nextcloud/axios'
 
 export const defaultLimit = loadState('unified-search', 'limit-default')
 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
  *
index 4569d1c46b6e80e1b445e5e9dd7a9b34cf1f1a4b..2d0b59db5dcb55f34bf8f257febf1c3dc1608d74 100644 (file)
                                @keypress.enter.prevent.stop="onInputEnter">
                </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">
                        <!-- Loading placeholders -->
                        <ul v-if="isLoading">
 </template>
 
 <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 Magnify from 'vue-material-design-icons/Magnify'
 import debounce from 'debounce'
 import { emit } from '@nextcloud/event-bus'
 
 import HeaderMenu from '../components/HeaderMenu'
+import SearchFilter from '../components/UnifiedSearch/SearchFilter'
 import SearchResult from '../components/UnifiedSearch/SearchResult'
 import SearchResultPlaceholder from '../components/UnifiedSearch/SearchResultPlaceholder'
 
@@ -114,6 +126,7 @@ export default {
                EmptyContent,
                HeaderMenu,
                Magnify,
+               SearchFilter,
                SearchResult,
                SearchResultPlaceholder,
        },
@@ -162,10 +175,10 @@ export default {
 
                /**
                 * Return ordered results
-                * @returns {Object}
+                * @returns {Array}
                 */
                orderedResults() {
-                       return Object.values(this.typesIDs)
+                       return this.typesIDs
                                .filter(type => type in this.results)
                                .map(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
                 * @returns {boolean}
@@ -291,12 +339,30 @@ export default {
                                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
                        this.resetState()
 
-                       this.typesIDs.forEach(async type => {
+                       types.forEach(async type => {
                                this.$set(this.loading, type, true)
-                               const request = await search(type, this.query)
+                               const request = await search(type, query)
 
                                // Process results
                                if (request.data.entries.length > 0) {
@@ -478,6 +544,13 @@ export default {
                                this.focused = index
                        }
                },
+
+               onClickFilter(filter) {
+                       this.query = `${this.query} ${filter}`
+                               .replace(/ {2}/g, ' ')
+                               .trim()
+                       this.onInput()
+               },
        },
 }
 </script>
@@ -500,6 +573,14 @@ $input-padding: 6px;
                background-color: var(--color-main-background);
        }
 
+       &__filters {
+               margin: $margin / 2 $margin;
+               ul {
+                       display: inline-flex;
+                       justify-content: space-between;
+               }
+       }
+
        &__input {
                // Minus margins
                width: calc(100% - 2 * #{$margin});
@@ -510,10 +591,9 @@ $input-padding: 6px;
                &[placeholder],
                &::placeholder {
                        overflow: hidden;
-                       text-overflow:ellipsis;
                        white-space: nowrap;
+                       text-overflow: ellipsis;
                }
-
        }
 
        &__results {