diff options
author | fenn-cs <fenn25.fn@gmail.com> | 2023-12-07 13:20:59 +0100 |
---|---|---|
committer | nextcloud-command <nextcloud-command@users.noreply.github.com> | 2023-12-22 13:13:03 +0000 |
commit | 525c2b82f6a3a6acec6312e368448e50274f7319 (patch) | |
tree | f53bc2130f1c5bccba83fbc6a96abfce79ef3e67 /core/src | |
parent | fecb3ea41cd9fe2e7f12fdb4b9b9a24845de281a (diff) | |
download | nextcloud-server-525c2b82f6a3a6acec6312e368448e50274f7319.tar.gz nextcloud-server-525c2b82f6a3a6acec6312e368448e50274f7319.zip |
Rename "global search" to "unified search"
- Changes appearances of "Global search" to "Unified search" in UI
- Refactors code, to remove usage of term "GlobalSearch" in files and code
structure
- Rename old unified search to `legacy-unified-search`
Signed-off-by: fenn-cs <fenn25.fn@gmail.com>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
Diffstat (limited to 'core/src')
14 files changed, 1344 insertions, 1346 deletions
diff --git a/core/src/components/GlobalSearch/SearchResult.vue b/core/src/components/GlobalSearch/SearchResult.vue deleted file mode 100644 index a746a5751b7..00000000000 --- a/core/src/components/GlobalSearch/SearchResult.vue +++ /dev/null @@ -1,169 +0,0 @@ -<template> - <NcListItem class="result-items__item" - :name="title" - :bold="false" - :href="resourceUrl" - target="_self"> - <template #icon> - <div aria-hidden="true" - class="result-items__item-icon" - :class="{ - 'result-items__item-icon--rounded': rounded, - 'result-items__item-icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl), - 'result-items__item-icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl), - [icon]: !isValidIconOrPreviewUrl(icon), - }" - :style="{ - backgroundImage: isValidIconOrPreviewUrl(icon) ? `url(${icon})` : '', - }"> - <img v-if="isValidIconOrPreviewUrl(thumbnailUrl) && !thumbnailHasError" - :src="thumbnailUrl" - @error="thumbnailErrorHandler"> - </div> - </template> - <template #subname> - {{ subline }} - </template> - </NcListItem> -</template> - -<script> -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' - -export default { - name: 'SearchResult', - components: { - NcListItem, - }, - props: { - thumbnailUrl: { - type: String, - default: null, - }, - title: { - type: String, - required: true, - }, - subline: { - type: String, - default: null, - }, - resourceUrl: { - type: String, - default: null, - }, - icon: { - type: String, - default: '', - }, - rounded: { - type: Boolean, - default: false, - }, - query: { - type: String, - default: '', - }, - - /** - * Only used for the first result as a visual feedback - * so we can keep the search input focused but pressing - * enter still opens the first result - */ - focused: { - type: Boolean, - default: false, - }, - }, - data() { - return { - thumbnailHasError: false, - } - }, - watch: { - thumbnailUrl() { - this.thumbnailHasError = false - }, - }, - methods: { - isValidIconOrPreviewUrl(url) { - return /^https?:\/\//.test(url) || url.startsWith('/') - }, - thumbnailErrorHandler() { - this.thumbnailHasError = true - }, - }, -} -</script> - -<style lang="scss" scoped> -@use "sass:math"; -$clickable-area: 44px; -$margin: 10px; - -.result-items { - &__item { - - ::v-deep a { - border-radius: 12px; - border: 2px solid transparent; - border-radius: var(--border-radius-large) !important; - - &--focused { - background-color: var(--color-background-hover); - } - - &:active, - &:hover, - &:focus { - background-color: var(--color-background-hover); - border: 2px solid var(--color-border-maxcontrast); - } - - * { - cursor: pointer; - } - - } - - &-icon { - overflow: hidden; - width: $clickable-area; - height: $clickable-area; - border-radius: var(--border-radius); - background-repeat: no-repeat; - background-position: center center; - background-size: 32px; - - &--rounded { - border-radius: math.div($clickable-area, 2); - } - - &--no-preview { - background-size: 32px; - } - - &--with-thumbnail { - background-size: cover; - } - - &--with-thumbnail:not(&--rounded) { - // compensate for border - max-width: $clickable-area - 2px; - max-height: $clickable-area - 2px; - border: 1px solid var(--color-border); - } - - img { - // Make sure to keep ratio - width: 100%; - height: 100%; - - object-fit: cover; - object-position: center; - } - } - - } -} -</style> diff --git a/core/src/components/GlobalSearch/CustomDateRangeModal.vue b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue index 0ba6ddca015..ec592732a8d 100644 --- a/core/src/components/GlobalSearch/CustomDateRangeModal.vue +++ b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue @@ -1,6 +1,6 @@ <template> <NcModal v-if="isModalOpen" - id="global-search" + id="unified-search" :name="t('core', 'Custom date range')" :show.sync="isModalOpen" :size="'small'" @@ -8,19 +8,19 @@ :title="t('core', 'Custom date range')" @close="closeModal"> <!-- Custom date range --> - <div class="global-search-custom-date-modal"> + <div class="unified-search-custom-date-modal"> <h1>{{ t('core', 'Custom date range') }}</h1> - <div class="global-search-custom-date-modal__pickers"> - <NcDateTimePicker :id="'globalsearch-custom-date-range-start'" + <div class="unified-search-custom-date-modal__pickers"> + <NcDateTimePicker :id="'unifiedsearch-custom-date-range-start'" v-model="dateFilter.startFrom" :label="t('core', 'Pick start date')" type="date" /> - <NcDateTimePicker :id="'globalsearch-custom-date-range-end'" + <NcDateTimePicker :id="'unifiedsearch-custom-date-range-end'" v-model="dateFilter.endAt" :label="t('core', 'Pick end date')" type="date" /> </div> - <div class="global-search-custom-date-modal__footer"> + <div class="unified-search-custom-date-modal__footer"> <NcButton @click="applyCustomRange"> {{ t('core', 'Search in date range') }} <template #icon> @@ -80,7 +80,7 @@ export default { </script> <style lang="scss" scoped> -.global-search-custom-date-modal { +.unified-search-custom-date-modal { padding: 10px 20px 10px 20px; h1 { diff --git a/core/src/components/UnifiedSearch/LegacySearchResult.vue b/core/src/components/UnifiedSearch/LegacySearchResult.vue new file mode 100644 index 00000000000..01f48a36709 --- /dev/null +++ b/core/src/components/UnifiedSearch/LegacySearchResult.vue @@ -0,0 +1,259 @@ + <!-- + - @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" + :class="{ + 'unified-search__result--focused': focused, + }" + @click="reEmitEvent" + @focus="reEmitEvent"> + + <!-- Icon describing the result --> + <div class="unified-search__result-icon" + :class="{ + 'unified-search__result-icon--rounded': rounded, + 'unified-search__result-icon--no-preview': !hasValidThumbnail && !loaded, + 'unified-search__result-icon--with-thumbnail': hasValidThumbnail && loaded, + [icon]: !loaded && !isIconUrl, + }" + :style="{ + backgroundImage: isIconUrl ? `url(${icon})` : '', + }"> + + <img v-if="hasValidThumbnail" + v-show="loaded" + :src="thumbnailUrl" + alt="" + @error="onError" + @load="onLoad"> + </div> + + <!-- Title and sub-title --> + <span class="unified-search__result-content"> + <span class="unified-search__result-line-one" :title="title"> + <NcHighlight :text="title" :search="query" /> + </span> + <span v-if="subline" class="unified-search__result-line-two" :title="subline">{{ subline }}</span> + </span> + </a> +</template> + +<script> +import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js' + +export default { + name: 'LegacySearchResult', + + components: { + NcHighlight, + }, + + props: { + thumbnailUrl: { + type: String, + default: null, + }, + title: { + type: String, + required: true, + }, + subline: { + type: String, + default: null, + }, + resourceUrl: { + type: String, + default: null, + }, + icon: { + type: String, + default: '', + }, + rounded: { + type: Boolean, + default: false, + }, + query: { + type: String, + default: '', + }, + + /** + * Only used for the first result as a visual feedback + * so we can keep the search input focused but pressing + * enter still opens the first result + */ + focused: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + hasValidThumbnail: this.thumbnailUrl && this.thumbnailUrl.trim() !== '', + loaded: false, + } + }, + + computed: { + isIconUrl() { + // If we're facing an absolute url + if (this.icon.startsWith('/')) { + return true + } + + // Otherwise, let's check if this is a valid url + try { + // eslint-disable-next-line no-new + new URL(this.icon) + } catch { + return false + } + return true + }, + }, + + watch: { + // Make sure to reset state on change even when vue recycle the component + thumbnailUrl() { + this.hasValidThumbnail = this.thumbnailUrl && this.thumbnailUrl.trim() !== '' + this.loaded = false + }, + }, + + methods: { + reEmitEvent(e) { + this.$emit(e.type, e) + }, + + /** + * If the image fails to load, fallback to iconClass + */ + onError() { + this.hasValidThumbnail = false + }, + + onLoad() { + this.loaded = true + }, + }, +} +</script> + +<style lang="scss" scoped> +@use "sass:math"; + +$clickable-area: 44px; +$margin: 10px; + +.unified-search__result { + display: flex; + align-items: center; + height: $clickable-area; + padding: $margin; + border: 2px solid transparent; + border-radius: var(--border-radius-large) !important; + + &--focused { + background-color: var(--color-background-hover); + } + + &:active, + &:hover, + &:focus { + background-color: var(--color-background-hover); + border: 2px solid var(--color-border-maxcontrast); + } + + * { + cursor: pointer; + } + + &-icon { + overflow: hidden; + width: $clickable-area; + height: $clickable-area; + border-radius: var(--border-radius); + background-repeat: no-repeat; + background-position: center center; + background-size: 32px; + &--rounded { + border-radius: math.div($clickable-area, 2); + } + &--no-preview { + background-size: 32px; + } + &--with-thumbnail { + background-size: cover; + } + &--with-thumbnail:not(&--rounded) { + // compensate for border + max-width: $clickable-area - 2px; + max-height: $clickable-area - 2px; + border: 1px solid var(--color-border); + } + + img { + // Make sure to keep ratio + width: 100%; + height: 100%; + + object-fit: cover; + object-position: center; + } + } + + &-icon, + &-actions { + flex: 0 0 $clickable-area; + } + + &-content { + display: flex; + align-items: center; + flex: 1 1 100%; + flex-wrap: wrap; + // Set to minimum and gro from it + min-width: 0; + padding-left: $margin; + } + + &-line-one, + &-line-two { + overflow: hidden; + flex: 1 1 100%; + margin: 1px 0; + white-space: nowrap; + text-overflow: ellipsis; + // Use the same color as the `a` + color: inherit; + font-size: inherit; + } + &-line-two { + opacity: .7; + font-size: var(--default-font-size); + } +} + +</style> diff --git a/core/src/components/GlobalSearch/SearchFilterChip.vue b/core/src/components/UnifiedSearch/SearchFilterChip.vue index 8342e9e256d..8342e9e256d 100644 --- a/core/src/components/GlobalSearch/SearchFilterChip.vue +++ b/core/src/components/UnifiedSearch/SearchFilterChip.vue diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue index 03496fc5d92..a746a5751b7 100644 --- a/core/src/components/UnifiedSearch/SearchResult.vue +++ b/core/src/components/UnifiedSearch/SearchResult.vue @@ -1,73 +1,40 @@ - <!-- - - @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" - :class="{ - 'unified-search__result--focused': focused, - }" - @click="reEmitEvent" - @focus="reEmitEvent"> - - <!-- Icon describing the result --> - <div class="unified-search__result-icon" - :class="{ - 'unified-search__result-icon--rounded': rounded, - 'unified-search__result-icon--no-preview': !hasValidThumbnail && !loaded, - 'unified-search__result-icon--with-thumbnail': hasValidThumbnail && loaded, - [icon]: !loaded && !isIconUrl, - }" - :style="{ - backgroundImage: isIconUrl ? `url(${icon})` : '', - }"> - - <img v-if="hasValidThumbnail" - v-show="loaded" - :src="thumbnailUrl" - alt="" - @error="onError" - @load="onLoad"> - </div> - - <!-- Title and sub-title --> - <span class="unified-search__result-content"> - <span class="unified-search__result-line-one" :title="title"> - <NcHighlight :text="title" :search="query" /> - </span> - <span v-if="subline" class="unified-search__result-line-two" :title="subline">{{ subline }}</span> - </span> - </a> + <NcListItem class="result-items__item" + :name="title" + :bold="false" + :href="resourceUrl" + target="_self"> + <template #icon> + <div aria-hidden="true" + class="result-items__item-icon" + :class="{ + 'result-items__item-icon--rounded': rounded, + 'result-items__item-icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl), + 'result-items__item-icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl), + [icon]: !isValidIconOrPreviewUrl(icon), + }" + :style="{ + backgroundImage: isValidIconOrPreviewUrl(icon) ? `url(${icon})` : '', + }"> + <img v-if="isValidIconOrPreviewUrl(thumbnailUrl) && !thumbnailHasError" + :src="thumbnailUrl" + @error="thumbnailErrorHandler"> + </div> + </template> + <template #subname> + {{ subline }} + </template> + </NcListItem> </template> <script> -import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js' +import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' export default { name: 'SearchResult', - components: { - NcHighlight, + NcListItem, }, - props: { thumbnailUrl: { type: String, @@ -108,54 +75,22 @@ export default { default: false, }, }, - data() { return { - hasValidThumbnail: this.thumbnailUrl && this.thumbnailUrl.trim() !== '', - loaded: false, + thumbnailHasError: false, } }, - - computed: { - isIconUrl() { - // If we're facing an absolute url - if (this.icon.startsWith('/')) { - return true - } - - // Otherwise, let's check if this is a valid url - try { - // eslint-disable-next-line no-new - new URL(this.icon) - } catch { - return false - } - return true - }, - }, - watch: { - // Make sure to reset state on change even when vue recycle the component thumbnailUrl() { - this.hasValidThumbnail = this.thumbnailUrl && this.thumbnailUrl.trim() !== '' - this.loaded = false + this.thumbnailHasError = false }, }, - methods: { - reEmitEvent(e) { - this.$emit(e.type, e) - }, - - /** - * If the image fails to load, fallback to iconClass - */ - onError() { - this.hasValidThumbnail = false + isValidIconOrPreviewUrl(url) { + return /^https?:\/\//.test(url) || url.startsWith('/') }, - - onLoad() { - this.loaded = true + thumbnailErrorHandler() { + this.thumbnailHasError = true }, }, } @@ -163,97 +98,72 @@ export default { <style lang="scss" scoped> @use "sass:math"; - $clickable-area: 44px; $margin: 10px; -.unified-search__result { - display: flex; - align-items: center; - height: $clickable-area; - padding: $margin; - border: 2px solid transparent; - border-radius: var(--border-radius-large) !important; - - &--focused { - background-color: var(--color-background-hover); - } - - &:active, - &:hover, - &:focus { - background-color: var(--color-background-hover); - border: 2px solid var(--color-border-maxcontrast); - } - - * { - cursor: pointer; - } - - &-icon { - overflow: hidden; - width: $clickable-area; - height: $clickable-area; - border-radius: var(--border-radius); - background-repeat: no-repeat; - background-position: center center; - background-size: 32px; - &--rounded { - border-radius: math.div($clickable-area, 2); - } - &--no-preview { - background-size: 32px; - } - &--with-thumbnail { - background-size: cover; - } - &--with-thumbnail:not(&--rounded) { - // compensate for border - max-width: $clickable-area - 2px; - max-height: $clickable-area - 2px; - border: 1px solid var(--color-border); - } - - img { - // Make sure to keep ratio - width: 100%; - height: 100%; - - object-fit: cover; - object-position: center; - } - } - - &-icon, - &-actions { - flex: 0 0 $clickable-area; - } - - &-content { - display: flex; - align-items: center; - flex: 1 1 100%; - flex-wrap: wrap; - // Set to minimum and gro from it - min-width: 0; - padding-left: $margin; - } - - &-line-one, - &-line-two { - overflow: hidden; - flex: 1 1 100%; - margin: 1px 0; - white-space: nowrap; - text-overflow: ellipsis; - // Use the same color as the `a` - color: inherit; - font-size: inherit; - } - &-line-two { - opacity: .7; - font-size: var(--default-font-size); - } +.result-items { + &__item { + + ::v-deep a { + border-radius: 12px; + border: 2px solid transparent; + border-radius: var(--border-radius-large) !important; + + &--focused { + background-color: var(--color-background-hover); + } + + &:active, + &:hover, + &:focus { + background-color: var(--color-background-hover); + border: 2px solid var(--color-border-maxcontrast); + } + + * { + cursor: pointer; + } + + } + + &-icon { + overflow: hidden; + width: $clickable-area; + height: $clickable-area; + border-radius: var(--border-radius); + background-repeat: no-repeat; + background-position: center center; + background-size: 32px; + + &--rounded { + border-radius: math.div($clickable-area, 2); + } + + &--no-preview { + background-size: 32px; + } + + &--with-thumbnail { + background-size: cover; + } + + &--with-thumbnail:not(&--rounded) { + // compensate for border + max-width: $clickable-area - 2px; + max-height: $clickable-area - 2px; + border: 1px solid var(--color-border); + } + + img { + // Make sure to keep ratio + width: 100%; + height: 100%; + + object-fit: cover; + object-position: center; + } + } + + } } - </style> diff --git a/core/src/components/GlobalSearch/SearchableList.vue b/core/src/components/UnifiedSearch/SearchableList.vue index 43f7ace1b64..43f7ace1b64 100644 --- a/core/src/components/GlobalSearch/SearchableList.vue +++ b/core/src/components/UnifiedSearch/SearchableList.vue diff --git a/core/src/global-search.js b/core/src/legacy-unified-search.js index f0c47fa1895..943081f3d23 100644 --- a/core/src/global-search.js +++ b/core/src/legacy-unified-search.js @@ -1,7 +1,7 @@ /** - * @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com> + * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> * - * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com> + * @author John Molakvoæ <skjnldsv@protonmail.com> * * @license AGPL-3.0-or-later * @@ -25,13 +25,13 @@ import { getRequestToken } from '@nextcloud/auth' import { translate as t, translatePlural as n } from '@nextcloud/l10n' import Vue from 'vue' -import GlobalSearch from './views/GlobalSearch.vue' +import UnifiedSearch from './views/LegacyUnifiedSearch.vue' // eslint-disable-next-line camelcase __webpack_nonce__ = btoa(getRequestToken()) const logger = getLoggerBuilder() - .setApp('global-search') + .setApp('unified-search') .detectUser() .build() @@ -48,8 +48,8 @@ Vue.mixin({ }) export default new Vue({ - el: '#global-search', + el: '#unified-search', // eslint-disable-next-line vue/match-component-file-name - name: 'GlobalSearchRoot', - render: h => h(GlobalSearch), + name: 'UnifiedSearchRoot', + render: h => h(UnifiedSearch), }) diff --git a/core/src/services/GlobalSearchService.js b/core/src/services/LegacyUnifiedSearchService.js index e477a59eb4c..3c673479771 100644 --- a/core/src/services/GlobalSearchService.js +++ b/core/src/services/LegacyUnifiedSearchService.js @@ -1,7 +1,10 @@ /** - * @copyright 2023, Fon E. Noel NFEBE <fenn25.fn@gmail.com> + * @copyright 2020, John Molakvoæ <skjnldsv@protonmail.com> * - * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com> + * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Daniel Calviño Sánchez <danxuliu@gmail.com> + * @author Joas Schilling <coding@schilljs.com> + * @author John Molakvoæ <skjnldsv@protonmail.com> * * @license AGPL-3.0-or-later * @@ -20,9 +23,17 @@ * */ -import { generateOcsUrl, generateUrl } from '@nextcloud/router' +import { generateOcsUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' import axios from '@nextcloud/axios' +export const defaultLimit = loadState('unified-search', 'limit-default') +export const minSearchLength = loadState('unified-search', 'min-search-length', 1) +export const enableLiveSearch = loadState('unified-search', 'live-search', true) + +export const regexFilterIn = /(^|\s)in:([a-z_-]+)/ig +export const regexFilterNot = /(^|\s)-in:([a-z_-]+)/ig + /** * Create a cancel token * @@ -35,7 +46,7 @@ const createCancelToken = () => axios.CancelToken.source() * * @return {Promise<Array>} */ -export async function getProviders() { +export async function getTypes() { try { const { data } = await axios.get(generateOcsUrl('search/providers'), { params: { @@ -60,13 +71,9 @@ export async function getProviders() { * @param {string} options.type the type to search * @param {string} options.query the search * @param {number|string|undefined} options.cursor the offset for paginated searches - * @param {string} options.since the search - * @param {string} options.until the search - * @param {string} options.limit the search - * @param {string} options.person the search * @return {object} {request: Promise, cancel: Promise} */ -export function search({ type, query, cursor, since, until, limit, person }) { +export function search({ type, query, cursor }) { /** * Generate an axios cancel token */ @@ -77,10 +84,6 @@ export function search({ type, query, cursor, since, until, limit, person }) { params: { term: query, cursor, - since, - until, - limit, - person, // Sending which location we're currently at from: window.location.pathname.replace('/index.php', '') + window.location.search, }, @@ -91,17 +94,3 @@ export function search({ type, query, cursor, since, until, limit, person }) { cancel: cancelToken.cancel, } } - -/** - * Get the list of active contacts - * - * @param {object} filter filter contacts by string - * @param filter.searchTerm - * @return {object} {request: Promise} - */ -export async function getContacts({ searchTerm }) { - const { data: { contacts } } = await axios.post(generateUrl('/contactsmenu/contacts'), { - filter: searchTerm, - }) - return contacts -} diff --git a/core/src/services/UnifiedSearchService.js b/core/src/services/UnifiedSearchService.js index 3c673479771..e477a59eb4c 100644 --- a/core/src/services/UnifiedSearchService.js +++ b/core/src/services/UnifiedSearchService.js @@ -1,10 +1,7 @@ /** - * @copyright 2020, John Molakvoæ <skjnldsv@protonmail.com> + * @copyright 2023, Fon E. Noel NFEBE <fenn25.fn@gmail.com> * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> + * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com> * * @license AGPL-3.0-or-later * @@ -23,17 +20,9 @@ * */ -import { generateOcsUrl } from '@nextcloud/router' -import { loadState } from '@nextcloud/initial-state' +import { generateOcsUrl, generateUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' -export const defaultLimit = loadState('unified-search', 'limit-default') -export const minSearchLength = loadState('unified-search', 'min-search-length', 1) -export const enableLiveSearch = loadState('unified-search', 'live-search', true) - -export const regexFilterIn = /(^|\s)in:([a-z_-]+)/ig -export const regexFilterNot = /(^|\s)-in:([a-z_-]+)/ig - /** * Create a cancel token * @@ -46,7 +35,7 @@ const createCancelToken = () => axios.CancelToken.source() * * @return {Promise<Array>} */ -export async function getTypes() { +export async function getProviders() { try { const { data } = await axios.get(generateOcsUrl('search/providers'), { params: { @@ -71,9 +60,13 @@ export async function getTypes() { * @param {string} options.type the type to search * @param {string} options.query the search * @param {number|string|undefined} options.cursor the offset for paginated searches + * @param {string} options.since the search + * @param {string} options.until the search + * @param {string} options.limit the search + * @param {string} options.person the search * @return {object} {request: Promise, cancel: Promise} */ -export function search({ type, query, cursor }) { +export function search({ type, query, cursor, since, until, limit, person }) { /** * Generate an axios cancel token */ @@ -84,6 +77,10 @@ export function search({ type, query, cursor }) { params: { term: query, cursor, + since, + until, + limit, + person, // Sending which location we're currently at from: window.location.pathname.replace('/index.php', '') + window.location.search, }, @@ -94,3 +91,17 @@ export function search({ type, query, cursor }) { cancel: cancelToken.cancel, } } + +/** + * Get the list of active contacts + * + * @param {object} filter filter contacts by string + * @param filter.searchTerm + * @return {object} {request: Promise} + */ +export async function getContacts({ searchTerm }) { + const { data: { contacts } } = await axios.post(generateUrl('/contactsmenu/contacts'), { + filter: searchTerm, + }) + return contacts +} diff --git a/core/src/unified-search.js b/core/src/unified-search.js index cc390c0d6e7..f9bddff4c68 100644 --- a/core/src/unified-search.js +++ b/core/src/unified-search.js @@ -1,7 +1,7 @@ /** - * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> + * @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com> * - * @author John Molakvoæ <skjnldsv@protonmail.com> + * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com> * * @license AGPL-3.0-or-later * diff --git a/core/src/views/GlobalSearch.vue b/core/src/views/GlobalSearch.vue deleted file mode 100644 index 09e2d4f725b..00000000000 --- a/core/src/views/GlobalSearch.vue +++ /dev/null @@ -1,96 +0,0 @@ - <!-- - - @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com> - - - - @author Fon E. Noel NFEBE <fenn25.fn@gmail.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> - <div class="header-menu"> - <NcButton class="global-search__button" :aria-label="t('core', 'Unified search')" @click="toggleGlobalSearch"> - <template #icon> - <Magnify class="global-search__trigger" :size="22" /> - </template> - </NcButton> - <GlobalSearchModal :class="'global-search-modal'" :is-visible="showGlobalSearch" @update:isVisible="handleModalVisibilityChange" /> - </div> -</template> - -<script> -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import Magnify from 'vue-material-design-icons/Magnify.vue' -import GlobalSearchModal from './GlobalSearchModal.vue' - -export default { - name: 'GlobalSearch', - components: { - NcButton, - Magnify, - GlobalSearchModal, - }, - data() { - return { - showGlobalSearch: false, - } - }, - mounted() { - console.debug('Global search initialized!') - }, - methods: { - toggleGlobalSearch() { - this.showGlobalSearch = !this.showGlobalSearch - }, - handleModalVisibilityChange(newVisibilityVal) { - this.showGlobalSearch = newVisibilityVal - }, - }, -} -</script> - -<style lang="scss" scoped> -.header-menu { - display: flex; - align-items: center; - justify-content: center; - - .global-search__button { - display: flex; - align-items: center; - justify-content: center; - width: var(--header-height); - // height: var(--header-height); - margin: 0; - padding: 0; - cursor: pointer; - opacity: .85; - background-color: transparent; - border: none; - filter: none !important; - color: var(--color-primary-text) !important; - - &:hover { - background-color: transparent !important; - } - } -} - -.global-search-modal { - ::v-deep .modal-container { - height: 80%; - } -} -</style> diff --git a/core/src/views/LegacyUnifiedSearch.vue b/core/src/views/LegacyUnifiedSearch.vue new file mode 100644 index 00000000000..04e4c77fe39 --- /dev/null +++ b/core/src/views/LegacyUnifiedSearch.vue @@ -0,0 +1,863 @@ + <!-- + - @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> + <NcHeaderMenu id="unified-search" + class="unified-search" + :exclude-click-outside-selectors="['.popover']" + :open.sync="open" + :aria-label="ariaLabel" + @open="onOpen" + @close="onClose"> + <!-- Header icon --> + <template #trigger> + <Magnify class="unified-search__trigger" + :size="22/* fit better next to other 20px icons */" /> + </template> + + <!-- Search form & filters wrapper --> + <div class="unified-search__input-wrapper"> + <div class="unified-search__input-row"> + <NcTextField ref="input" + :value.sync="query" + trailing-button-icon="close" + :label="ariaLabel" + :trailing-button-label="t('core','Reset search')" + :show-trailing-button="query !== ''" + aria-describedby="unified-search-desc" + class="unified-search__form-input" + :class="{'unified-search__form-input--with-reset': !!query}" + :placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ') })" + @trailing-button-click="onReset" + @input="onInputDebounced" /> + <p id="unified-search-desc" class="hidden-visually"> + {{ t('core', 'Search starts once you start typing and results may be reached with the arrow keys') }} + </p> + + <!-- Search filters --> + <NcActions v-if="availableFilters.length > 1" + class="unified-search__filters" + placement="bottom-end" + container=".unified-search__input-wrapper"> + <!-- FIXME use element ref for container after https://github.com/nextcloud/nextcloud-vue/pull/3462 --> + <NcActionButton v-for="filter in availableFilters" + :key="filter" + icon="icon-filter" + @click.stop="onClickFilter(`in:${filter}`)"> + {{ t('core', 'Search for {name} only', { name: typesMap[filter] }) }} + </NcActionButton> + </NcActions> + </div> + </div> + + <template v-if="!hasResults"> + <!-- Loading placeholders --> + <SearchResultPlaceholders v-if="isLoading" /> + + <NcEmptyContent v-else-if="isValidQuery" + :title="validQueryTitle"> + <template #icon> + <Magnify /> + </template> + </NcEmptyContent> + + <NcEmptyContent v-else-if="!isLoading || isShortQuery" + :title="t('core', 'Start typing to search')" + :description="shortQueryDescription"> + <template #icon> + <Magnify /> + </template> + </NcEmptyContent> + </template> + + <!-- Grouped search results --> + <template v-for="({list, type}, typesIndex) in orderedResults" v-else> + <h2 :key="type" class="unified-search__results-header"> + {{ typesMap[type] }} + </h2> + <ul :key="type" + class="unified-search__results" + :class="`unified-search__results-${type}`" + :aria-label="typesMap[type]"> + <!-- Search results --> + <li v-for="(result, index) in limitIfAny(list, type)" :key="result.resourceUrl"> + <SearchResult v-bind="result" + :query="query" + :focused="focused === 0 && typesIndex === 0 && index === 0" + @focus="setFocusedIndex" /> + </li> + + <!-- Load more button --> + <li> + <SearchResult v-if="!reached[type]" + class="unified-search__result-more" + :title="loading[type] + ? t('core', 'Loading more results …') + : t('core', 'Load more results')" + :icon-class="loading[type] ? 'icon-loading-small' : ''" + @click.prevent.stop="loadMore(type)" + @focus="setFocusedIndex" /> + </li> + </ul> + </template> + </NcHeaderMenu> +</template> + +<script> +import debounce from 'debounce' +import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' +import { showError } from '@nextcloud/dialogs' + +import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' +import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' +import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' +import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js' +import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' + +import Magnify from 'vue-material-design-icons/Magnify.vue' + +import SearchResult from '../components/UnifiedSearch/LegacySearchResult.vue' +import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders.vue' + +import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot, enableLiveSearch } from '../services/LegacyUnifiedSearchService.js' + +const REQUEST_FAILED = 0 +const REQUEST_OK = 1 +const REQUEST_CANCELED = 2 + +export default { + name: 'LegacyUnifiedSearch', + + components: { + Magnify, + NcActionButton, + NcActions, + NcEmptyContent, + NcHeaderMenu, + SearchResult, + SearchResultPlaceholders, + NcTextField, + }, + + data() { + return { + types: [], + + // Cursors per types + cursors: {}, + // Various search limits per types + limits: {}, + // Loading types + loading: {}, + // Reached search types + reached: {}, + // Pending cancellable requests + requests: [], + // List of all results + results: {}, + + query: '', + focused: null, + triggered: false, + + defaultLimit, + minSearchLength, + enableLiveSearch, + + open: false, + } + }, + + computed: { + typesIDs() { + return this.types.map(type => type.id) + }, + typesNames() { + return this.types.map(type => type.name) + }, + typesMap() { + return this.types.reduce((prev, curr) => { + prev[curr.id] = curr.name + return prev + }, {}) + }, + + ariaLabel() { + return t('core', 'Search') + }, + + /** + * Is there any result to display + * + * @return {boolean} + */ + hasResults() { + return Object.keys(this.results).length !== 0 + }, + + /** + * Return ordered results + * + * @return {Array} + */ + orderedResults() { + return this.typesIDs + .filter(type => type in this.results) + .map(type => ({ + type, + list: this.results[type], + })) + }, + + /** + * Available filters + * We only show filters that are available on the results + * + * @return {string[]} + */ + availableFilters() { + return Object.keys(this.results) + }, + + /** + * Applied filters + * + * @return {string[]} + */ + usedFiltersIn() { + let match + const filters = [] + while ((match = regexFilterIn.exec(this.query)) !== null) { + filters.push(match[2]) + } + return filters + }, + + /** + * Applied anti filters + * + * @return {string[]} + */ + usedFiltersNot() { + let match + const filters = [] + while ((match = regexFilterNot.exec(this.query)) !== null) { + filters.push(match[2]) + } + return filters + }, + + /** + * Valid query empty content title + * + * @return {string} + */ + validQueryTitle() { + return this.triggered + ? t('core', 'No results for {query}', { query: this.query }) + : t('core', 'Press Enter to start searching') + }, + + /** + * Short query empty content description + * + * @return {string} + */ + shortQueryDescription() { + if (!this.isShortQuery) { + return '' + } + + return n('core', + 'Please enter {minSearchLength} character or more to search', + 'Please enter {minSearchLength} characters or more to search', + this.minSearchLength, + { minSearchLength: this.minSearchLength }) + }, + + /** + * Is the current search too short + * + * @return {boolean} + */ + isShortQuery() { + return this.query && this.query.trim().length < minSearchLength + }, + + /** + * Is the current search valid + * + * @return {boolean} + */ + isValidQuery() { + return this.query && this.query.trim() !== '' && !this.isShortQuery + }, + + /** + * Have we reached the end of all types searches + * + * @return {boolean} + */ + isDoneSearching() { + return Object.values(this.reached).every(state => state === false) + }, + + /** + * Is there any search in progress + * + * @return {boolean} + */ + isLoading() { + return Object.values(this.loading).some(state => state === true) + }, + }, + + async created() { + this.types = await getTypes() + this.logger.debug('Unified Search initialized with the following providers', this.types) + }, + + beforeDestroy() { + unsubscribe('files:navigation:changed', this.onNavigationChange) + }, + + mounted() { + // subscribe in mounted, as onNavigationChange relys on $el + subscribe('files:navigation:changed', this.onNavigationChange) + + if (OCP.Accessibility.disableKeyboardShortcuts()) { + return + } + + document.addEventListener('keydown', (event) => { + // if not already opened, allows us to trigger default browser on second keydown + if (event.ctrlKey && event.code === 'KeyF' && !this.open) { + event.preventDefault() + this.open = true + } else if (event.ctrlKey && event.key === 'f' && this.open) { + // User wants to use the native browser search, so we close ours again + this.open = false + } + + // https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus + if (this.open) { + // If arrow down, focus next result + if (event.key === 'ArrowDown') { + this.focusNext(event) + } + + // If arrow up, focus prev result + if (event.key === 'ArrowUp') { + this.focusPrev(event) + } + } + }) + }, + + methods: { + async onOpen() { + // Update types list in the background + this.types = await getTypes() + }, + onClose() { + emit('nextcloud:unified-search.close') + }, + + onNavigationChange() { + this.$el?.querySelector?.('form[role="search"]')?.reset?.() + }, + + /** + * Reset the search state + */ + onReset() { + emit('nextcloud:unified-search.reset') + this.logger.debug('Search reset') + this.query = '' + this.resetState() + this.focusInput() + }, + async resetState() { + this.cursors = {} + this.limits = {} + this.reached = {} + this.results = {} + this.focused = null + this.triggered = false + await this.cancelPendingRequests() + }, + + /** + * Cancel any ongoing searches + */ + async cancelPendingRequests() { + // Cloning so we can keep processing other requests + const requests = this.requests.slice(0) + this.requests = [] + + // Cancel all pending requests + await Promise.all(requests.map(cancel => cancel())) + }, + + /** + * Focus the search input on next tick + */ + focusInput() { + this.$nextTick(() => { + this.$refs.input.focus() + this.$refs.input.select() + }) + }, + + /** + * If we have results already, open first one + * If not, trigger the search again + */ + onInputEnter() { + if (this.hasResults) { + const results = this.getResultsList() + results[0].click() + return + } + this.onInput() + }, + + /** + * Start searching on input + */ + async onInput() { + // emit the search query + emit('nextcloud:unified-search.search', { query: this.query }) + + // Do not search if not long enough + if (this.query.trim() === '' || this.isShortQuery) { + for (const type of this.typesIDs) { + this.$delete(this.results, type) + } + 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, '') + + // Reset search if the query changed + await this.resetState() + this.triggered = true + + if (!types.length) { + // no results since no types were selected + this.logger.error('No types to search in') + return + } + + this.$set(this.loading, 'all', true) + this.logger.debug(`Searching ${query} in`, types) + + Promise.all(types.map(async type => { + try { + // Init cancellable request + const { request, cancel } = search({ type, query }) + this.requests.push(cancel) + + // Fetch results + const { data } = await request() + + // Process results + if (data.ocs.data.entries.length > 0) { + this.$set(this.results, type, data.ocs.data.entries) + } else { + this.$delete(this.results, type) + } + + // Save cursor if any + if (data.ocs.data.cursor) { + this.$set(this.cursors, type, data.ocs.data.cursor) + } else if (!data.ocs.data.isPaginated) { + // If no cursor and no pagination, we save the default amount + // provided by server's initial state `defaultLimit` + this.$set(this.limits, type, this.defaultLimit) + } + + // Check if we reached end of pagination + if (data.ocs.data.entries.length < this.defaultLimit) { + this.$set(this.reached, type, true) + } + + // If none already focused, focus the first rendered result + if (this.focused === null) { + this.focused = 0 + } + return REQUEST_OK + } catch (error) { + this.$delete(this.results, type) + + // If this is not a cancelled throw + if (error.response && error.response.status) { + this.logger.error(`Error searching for ${this.typesMap[type]}`, error) + showError(this.t('core', 'An error occurred while searching for {type}', { type: this.typesMap[type] })) + return REQUEST_FAILED + } + return REQUEST_CANCELED + } + })).then(results => { + // Do not declare loading finished if the request have been cancelled + // This means another search was triggered and we're therefore still loading + if (results.some(result => result === REQUEST_CANCELED)) { + return + } + // We finished all searches + this.loading = {} + }) + }, + onInputDebounced: enableLiveSearch + ? debounce(function(e) { + this.onInput(e) + }, 500) + : function() { + this.triggered = false + }, + + /** + * Load more results for the provided type + * + * @param {string} type type + */ + async loadMore(type) { + // If already loading, ignore + if (this.loading[type]) { + return + } + + if (this.cursors[type]) { + // Init cancellable request + const { request, cancel } = search({ type, query: this.query, cursor: this.cursors[type] }) + this.requests.push(cancel) + + // Fetch results + const { data } = await request() + + // Save cursor if any + if (data.ocs.data.cursor) { + this.$set(this.cursors, type, data.ocs.data.cursor) + } + + // Process results + if (data.ocs.data.entries.length > 0) { + this.results[type].push(...data.ocs.data.entries) + } + + // Check if we reached end of pagination + if (data.ocs.data.entries.length < this.defaultLimit) { + this.$set(this.reached, type, true) + } + } else { + // If no cursor, we might have all the results already, + // let's fake pagination and show the next xxx entries + if (this.limits[type] && this.limits[type] >= 0) { + this.limits[type] += this.defaultLimit + + // Check if we reached end of pagination + if (this.limits[type] >= this.results[type].length) { + this.$set(this.reached, type, true) + } + } + } + + // Focus result after render + if (this.focused !== null) { + this.$nextTick(() => { + this.focusIndex(this.focused) + }) + } + }, + + /** + * Return a subset of the array if the search provider + * doesn't supports pagination + * + * @param {Array} list the results + * @param {string} type the type + * @return {Array} + */ + limitIfAny(list, type) { + if (type in this.limits) { + return list.slice(0, this.limits[type]) + } + return list + }, + + getResultsList() { + return this.$el.querySelectorAll('.unified-search__results .unified-search__result') + }, + + /** + * Focus the first result if any + * + * @param {Event} event the keydown event + */ + focusFirst(event) { + const results = this.getResultsList() + if (results && results.length > 0) { + if (event) { + event.preventDefault() + } + this.focused = 0 + this.focusIndex(this.focused) + } + }, + + /** + * Focus the next result if any + * + * @param {Event} event the keydown event + */ + focusNext(event) { + if (this.focused === null) { + this.focusFirst(event) + return + } + + const results = this.getResultsList() + // If we're not focusing the last, focus the next one + if (results && results.length > 0 && this.focused + 1 < results.length) { + event.preventDefault() + this.focused++ + this.focusIndex(this.focused) + } + }, + + /** + * Focus the previous result if any + * + * @param {Event} event the keydown event + */ + focusPrev(event) { + if (this.focused === null) { + this.focusFirst(event) + return + } + + const results = this.getResultsList() + // If we're not focusing the first, focus the previous one + if (results && results.length > 0 && this.focused > 0) { + event.preventDefault() + this.focused-- + this.focusIndex(this.focused) + } + + }, + + /** + * Focus the specified result index if it exists + * + * @param {number} index the result index + */ + focusIndex(index) { + const results = this.getResultsList() + if (results && results[index]) { + results[index].focus() + } + }, + + /** + * Set the current focused element based on the target + * + * @param {Event} event the focus event + */ + setFocusedIndex(event) { + const entry = event.target + const results = this.getResultsList() + const index = [...results].findIndex(search => search === entry) + if (index > -1) { + // let's not use focusIndex as the entry is already focused + this.focused = index + } + }, + + onClickFilter(filter) { + this.query = `${this.query} ${filter}` + .replace(/ {2}/g, ' ') + .trim() + this.onInput() + }, + }, +} +</script> + +<style lang="scss" scoped> +@use "sass:math"; + +$margin: 10px; +$input-height: 34px; +$input-padding: 10px; + +.unified-search { + &__input-wrapper { + position: sticky; + // above search results + z-index: 2; + top: 0; + display: inline-flex; + flex-direction: column; + align-items: center; + width: 100%; + background-color: var(--color-main-background); + + label[for="unified-search__input"] { + align-self: flex-start; + font-weight: bold; + font-size: 19px; + margin-left: 13px; + } + } + + &__form-input { + margin: 0 !important; + &:focus, + &:focus-visible, + &:active { + border-color: 2px solid var(--color-main-text) !important; + box-shadow: 0 0 0 2px var(--color-main-background) !important; + } + } + + &__input-row { + display: flex; + width: 100%; + align-items: center; + } + + &__filters { + margin: $margin 0 $margin math.div($margin, 2); + padding-top: 5px; + ul { + display: inline-flex; + justify-content: space-between; + } + } + + &__form { + position: relative; + width: 100%; + margin: $margin 0; + + // Loading spinner + &::after { + right: $input-padding; + left: auto; + } + + &-input, + &-reset { + margin: math.div($input-padding, 2); + } + + &-input { + width: 100%; + height: $input-height; + padding: $input-padding; + + &, + &[placeholder], + &::placeholder { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + // Hide webkit clear search + &::-webkit-search-decoration, + &::-webkit-search-cancel-button, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + -webkit-appearance: none; + } + } + + &-reset, &-submit { + position: absolute; + top: 0; + right: 4px; + width: $input-height - $input-padding; + height: $input-height - $input-padding; + min-height: 30px; + padding: 0; + opacity: .5; + border: none; + background-color: transparent; + margin-right: 0; + + &:hover, + &:focus, + &:active { + opacity: 1; + } + } + + &-submit { + right: 28px; + } + } + + &__results { + &-header { + display: block; + margin: $margin; + margin-bottom: $margin - 4px; + margin-left: 13px; + color: var(--color-primary-element); + font-size: 19px; + font-weight: bold; + } + display: flex; + flex-direction: column; + gap: 4px; + } + + .unified-search__result-more::v-deep { + color: var(--color-text-maxcontrast); + } + + .empty-content { + margin: 10vh 0; + + ::v-deep .empty-content__title { + font-weight: normal; + font-size: var(--default-font-size); + text-align: center; + } + } +} + +</style> diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue index e87d7dc6a63..419c0b47c41 100644 --- a/core/src/views/UnifiedSearch.vue +++ b/core/src/views/UnifiedSearch.vue @@ -1,7 +1,7 @@ <!-- - - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> + - @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com> - - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Fon E. Noel NFEBE <fenn25.fn@gmail.com> - - @license GNU AGPL version 3 or any later version - @@ -20,845 +20,77 @@ - --> <template> - <NcHeaderMenu id="unified-search" - class="unified-search" - :exclude-click-outside-selectors="['.popover']" - :open.sync="open" - :aria-label="ariaLabel" - @open="onOpen" - @close="onClose"> - <!-- Header icon --> - <template #trigger> - <Magnify class="unified-search__trigger" - :size="22/* fit better next to other 20px icons */" /> - </template> - - <!-- Search form & filters wrapper --> - <div class="unified-search__input-wrapper"> - <div class="unified-search__input-row"> - <NcTextField ref="input" - :value.sync="query" - trailing-button-icon="close" - :label="ariaLabel" - :trailing-button-label="t('core','Reset search')" - :show-trailing-button="query !== ''" - aria-describedby="unified-search-desc" - class="unified-search__form-input" - :class="{'unified-search__form-input--with-reset': !!query}" - :placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ') })" - @trailing-button-click="onReset" - @input="onInputDebounced" /> - <p id="unified-search-desc" class="hidden-visually"> - {{ t('core', 'Search starts once you start typing and results may be reached with the arrow keys') }} - </p> - - <!-- Search filters --> - <NcActions v-if="availableFilters.length > 1" - class="unified-search__filters" - placement="bottom-end" - container=".unified-search__input-wrapper"> - <!-- FIXME use element ref for container after https://github.com/nextcloud/nextcloud-vue/pull/3462 --> - <NcActionButton v-for="filter in availableFilters" - :key="filter" - icon="icon-filter" - :title="t('core', 'Search for {name} only', { name: typesMap[filter] })" - @click.stop="onClickFilter(`in:${filter}`)"> - {{ `in:${filter}` }} - </NcActionButton> - </NcActions> - </div> - </div> - - <template v-if="!hasResults"> - <!-- Loading placeholders --> - <SearchResultPlaceholders v-if="isLoading" /> - - <NcEmptyContent v-else-if="isValidQuery" - :title="validQueryTitle"> - <template #icon> - <Magnify /> - </template> - </NcEmptyContent> - - <NcEmptyContent v-else-if="!isLoading || isShortQuery" - :title="t('core', 'Start typing to search')" - :description="shortQueryDescription"> - <template #icon> - <Magnify /> - </template> - </NcEmptyContent> - </template> - - <!-- Grouped search results --> - <template v-for="({list, type}, typesIndex) in orderedResults" v-else> - <h2 :key="type" class="unified-search__results-header"> - {{ typesMap[type] }} - </h2> - <ul :key="type" - class="unified-search__results" - :class="`unified-search__results-${type}`" - :aria-label="typesMap[type]"> - <!-- Search results --> - <li v-for="(result, index) in limitIfAny(list, type)" :key="result.resourceUrl"> - <SearchResult v-bind="result" - :query="query" - :focused="focused === 0 && typesIndex === 0 && index === 0" - @focus="setFocusedIndex" /> - </li> - - <!-- Load more button --> - <li> - <SearchResult v-if="!reached[type]" - class="unified-search__result-more" - :title="loading[type] - ? t('core', 'Loading more results …') - : t('core', 'Load more results')" - :icon-class="loading[type] ? 'icon-loading-small' : ''" - @click.prevent.stop="loadMore(type)" - @focus="setFocusedIndex" /> - </li> - </ul> - </template> - </NcHeaderMenu> + <div class="header-menu"> + <NcButton class="unified-search__button" :aria-label="t('core', 'Unified search')" @click="toggleUnifiedSearch"> + <template #icon> + <Magnify class="unified-search__trigger" :size="22" /> + </template> + </NcButton> + <UnifiedSearchModal :class="'unified-search-modal'" :is-visible="showUnifiedSearch" @update:isVisible="handleModalVisibilityChange" /> + </div> </template> <script> -import debounce from 'debounce' -import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' -import { showError } from '@nextcloud/dialogs' - -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' -import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' - +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import Magnify from 'vue-material-design-icons/Magnify.vue' - -import SearchResult from '../components/UnifiedSearch/SearchResult.vue' -import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders.vue' - -import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot, enableLiveSearch } from '../services/UnifiedSearchService.js' - -const REQUEST_FAILED = 0 -const REQUEST_OK = 1 -const REQUEST_CANCELED = 2 +import UnifiedSearchModal from './UnifiedSearchModal.vue' export default { name: 'UnifiedSearch', - components: { + NcButton, Magnify, - NcActionButton, - NcActions, - NcEmptyContent, - NcHeaderMenu, - SearchResult, - SearchResultPlaceholders, - NcTextField, + UnifiedSearchModal, }, - data() { return { - types: [], - - // Cursors per types - cursors: {}, - // Various search limits per types - limits: {}, - // Loading types - loading: {}, - // Reached search types - reached: {}, - // Pending cancellable requests - requests: [], - // List of all results - results: {}, - - query: '', - focused: null, - triggered: false, - - defaultLimit, - minSearchLength, - enableLiveSearch, - - open: false, + showUnifiedSearch: false, } }, - - computed: { - typesIDs() { - return this.types.map(type => type.id) - }, - typesNames() { - return this.types.map(type => type.name) - }, - typesMap() { - return this.types.reduce((prev, curr) => { - prev[curr.id] = curr.name - return prev - }, {}) - }, - - ariaLabel() { - return t('core', 'Search') - }, - - /** - * Is there any result to display - * - * @return {boolean} - */ - hasResults() { - return Object.keys(this.results).length !== 0 - }, - - /** - * Return ordered results - * - * @return {Array} - */ - orderedResults() { - return this.typesIDs - .filter(type => type in this.results) - .map(type => ({ - type, - list: this.results[type], - })) - }, - - /** - * Available filters - * We only show filters that are available on the results - * - * @return {string[]} - */ - availableFilters() { - return Object.keys(this.results) - }, - - /** - * Applied filters - * - * @return {string[]} - */ - usedFiltersIn() { - let match - const filters = [] - while ((match = regexFilterIn.exec(this.query)) !== null) { - filters.push(match[2]) - } - return filters - }, - - /** - * Applied anti filters - * - * @return {string[]} - */ - usedFiltersNot() { - let match - const filters = [] - while ((match = regexFilterNot.exec(this.query)) !== null) { - filters.push(match[2]) - } - return filters - }, - - /** - * Valid query empty content title - * - * @return {string} - */ - validQueryTitle() { - return this.triggered - ? t('core', 'No results for {query}', { query: this.query }) - : t('core', 'Press Enter to start searching') - }, - - /** - * Short query empty content description - * - * @return {string} - */ - shortQueryDescription() { - if (!this.isShortQuery) { - return '' - } - - return n('core', - 'Please enter {minSearchLength} character or more to search', - 'Please enter {minSearchLength} characters or more to search', - this.minSearchLength, - { minSearchLength: this.minSearchLength }) - }, - - /** - * Is the current search too short - * - * @return {boolean} - */ - isShortQuery() { - return this.query && this.query.trim().length < minSearchLength - }, - - /** - * Is the current search valid - * - * @return {boolean} - */ - isValidQuery() { - return this.query && this.query.trim() !== '' && !this.isShortQuery - }, - - /** - * Have we reached the end of all types searches - * - * @return {boolean} - */ - isDoneSearching() { - return Object.values(this.reached).every(state => state === false) - }, - - /** - * Is there any search in progress - * - * @return {boolean} - */ - isLoading() { - return Object.values(this.loading).some(state => state === true) - }, - }, - - async created() { - this.types = await getTypes() - this.logger.debug('Unified Search initialized with the following providers', this.types) - }, - - beforeDestroy() { - unsubscribe('files:navigation:changed', this.onNavigationChange) - }, - mounted() { - // subscribe in mounted, as onNavigationChange relys on $el - subscribe('files:navigation:changed', this.onNavigationChange) - - if (OCP.Accessibility.disableKeyboardShortcuts()) { - return - } - - document.addEventListener('keydown', (event) => { - // if not already opened, allows us to trigger default browser on second keydown - if (event.ctrlKey && event.code === 'KeyF' && !this.open) { - event.preventDefault() - this.open = true - } else if (event.ctrlKey && event.key === 'f' && this.open) { - // User wants to use the native browser search, so we close ours again - this.open = false - } - - // https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus - if (this.open) { - // If arrow down, focus next result - if (event.key === 'ArrowDown') { - this.focusNext(event) - } - - // If arrow up, focus prev result - if (event.key === 'ArrowUp') { - this.focusPrev(event) - } - } - }) + console.debug('Unified search initialized!') }, - methods: { - async onOpen() { - // Update types list in the background - this.types = await getTypes() - }, - onClose() { - emit('nextcloud:unified-search.close') - }, - - onNavigationChange() { - this.$el?.querySelector?.('form[role="search"]')?.reset?.() - }, - - /** - * Reset the search state - */ - onReset() { - emit('nextcloud:unified-search.reset') - this.logger.debug('Search reset') - this.query = '' - this.resetState() - this.focusInput() - }, - async resetState() { - this.cursors = {} - this.limits = {} - this.reached = {} - this.results = {} - this.focused = null - this.triggered = false - await this.cancelPendingRequests() - }, - - /** - * Cancel any ongoing searches - */ - async cancelPendingRequests() { - // Cloning so we can keep processing other requests - const requests = this.requests.slice(0) - this.requests = [] - - // Cancel all pending requests - await Promise.all(requests.map(cancel => cancel())) - }, - - /** - * Focus the search input on next tick - */ - focusInput() { - this.$nextTick(() => { - this.$refs.input.focus() - this.$refs.input.select() - }) - }, - - /** - * If we have results already, open first one - * If not, trigger the search again - */ - onInputEnter() { - if (this.hasResults) { - const results = this.getResultsList() - results[0].click() - return - } - this.onInput() - }, - - /** - * Start searching on input - */ - async onInput() { - // emit the search query - emit('nextcloud:unified-search.search', { query: this.query }) - - // Do not search if not long enough - if (this.query.trim() === '' || this.isShortQuery) { - for (const type of this.typesIDs) { - this.$delete(this.results, type) - } - 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, '') - - // Reset search if the query changed - await this.resetState() - this.triggered = true - - if (!types.length) { - // no results since no types were selected - this.logger.error('No types to search in') - return - } - - this.$set(this.loading, 'all', true) - this.logger.debug(`Searching ${query} in`, types) - - Promise.all(types.map(async type => { - try { - // Init cancellable request - const { request, cancel } = search({ type, query }) - this.requests.push(cancel) - - // Fetch results - const { data } = await request() - - // Process results - if (data.ocs.data.entries.length > 0) { - this.$set(this.results, type, data.ocs.data.entries) - } else { - this.$delete(this.results, type) - } - - // Save cursor if any - if (data.ocs.data.cursor) { - this.$set(this.cursors, type, data.ocs.data.cursor) - } else if (!data.ocs.data.isPaginated) { - // If no cursor and no pagination, we save the default amount - // provided by server's initial state `defaultLimit` - this.$set(this.limits, type, this.defaultLimit) - } - - // Check if we reached end of pagination - if (data.ocs.data.entries.length < this.defaultLimit) { - this.$set(this.reached, type, true) - } - - // If none already focused, focus the first rendered result - if (this.focused === null) { - this.focused = 0 - } - return REQUEST_OK - } catch (error) { - this.$delete(this.results, type) - - // If this is not a cancelled throw - if (error.response && error.response.status) { - this.logger.error(`Error searching for ${this.typesMap[type]}`, error) - showError(this.t('core', 'An error occurred while searching for {type}', { type: this.typesMap[type] })) - return REQUEST_FAILED - } - return REQUEST_CANCELED - } - })).then(results => { - // Do not declare loading finished if the request have been cancelled - // This means another search was triggered and we're therefore still loading - if (results.some(result => result === REQUEST_CANCELED)) { - return - } - // We finished all searches - this.loading = {} - }) - }, - onInputDebounced: enableLiveSearch - ? debounce(function(e) { - this.onInput(e) - }, 500) - : function() { - this.triggered = false - }, - - /** - * Load more results for the provided type - * - * @param {string} type type - */ - async loadMore(type) { - // If already loading, ignore - if (this.loading[type]) { - return - } - - if (this.cursors[type]) { - // Init cancellable request - const { request, cancel } = search({ type, query: this.query, cursor: this.cursors[type] }) - this.requests.push(cancel) - - // Fetch results - const { data } = await request() - - // Save cursor if any - if (data.ocs.data.cursor) { - this.$set(this.cursors, type, data.ocs.data.cursor) - } - - // Process results - if (data.ocs.data.entries.length > 0) { - this.results[type].push(...data.ocs.data.entries) - } - - // Check if we reached end of pagination - if (data.ocs.data.entries.length < this.defaultLimit) { - this.$set(this.reached, type, true) - } - } else { - // If no cursor, we might have all the results already, - // let's fake pagination and show the next xxx entries - if (this.limits[type] && this.limits[type] >= 0) { - this.limits[type] += this.defaultLimit - - // Check if we reached end of pagination - if (this.limits[type] >= this.results[type].length) { - this.$set(this.reached, type, true) - } - } - } - - // Focus result after render - if (this.focused !== null) { - this.$nextTick(() => { - this.focusIndex(this.focused) - }) - } - }, - - /** - * Return a subset of the array if the search provider - * doesn't supports pagination - * - * @param {Array} list the results - * @param {string} type the type - * @return {Array} - */ - limitIfAny(list, type) { - if (type in this.limits) { - return list.slice(0, this.limits[type]) - } - return list - }, - - getResultsList() { - return this.$el.querySelectorAll('.unified-search__results .unified-search__result') - }, - - /** - * Focus the first result if any - * - * @param {Event} event the keydown event - */ - focusFirst(event) { - const results = this.getResultsList() - if (results && results.length > 0) { - if (event) { - event.preventDefault() - } - this.focused = 0 - this.focusIndex(this.focused) - } - }, - - /** - * Focus the next result if any - * - * @param {Event} event the keydown event - */ - focusNext(event) { - if (this.focused === null) { - this.focusFirst(event) - return - } - - const results = this.getResultsList() - // If we're not focusing the last, focus the next one - if (results && results.length > 0 && this.focused + 1 < results.length) { - event.preventDefault() - this.focused++ - this.focusIndex(this.focused) - } - }, - - /** - * Focus the previous result if any - * - * @param {Event} event the keydown event - */ - focusPrev(event) { - if (this.focused === null) { - this.focusFirst(event) - return - } - - const results = this.getResultsList() - // If we're not focusing the first, focus the previous one - if (results && results.length > 0 && this.focused > 0) { - event.preventDefault() - this.focused-- - this.focusIndex(this.focused) - } - - }, - - /** - * Focus the specified result index if it exists - * - * @param {number} index the result index - */ - focusIndex(index) { - const results = this.getResultsList() - if (results && results[index]) { - results[index].focus() - } - }, - - /** - * Set the current focused element based on the target - * - * @param {Event} event the focus event - */ - setFocusedIndex(event) { - const entry = event.target - const results = this.getResultsList() - const index = [...results].findIndex(search => search === entry) - if (index > -1) { - // let's not use focusIndex as the entry is already focused - this.focused = index - } + toggleUnifiedSearch() { + this.showUnifiedSearch = !this.showUnifiedSearch }, - - onClickFilter(filter) { - this.query = `${this.query} ${filter}` - .replace(/ {2}/g, ' ') - .trim() - this.onInput() + handleModalVisibilityChange(newVisibilityVal) { + this.showUnifiedSearch = newVisibilityVal }, }, } </script> <style lang="scss" scoped> -@use "sass:math"; - -$margin: 10px; -$input-height: 34px; -$input-padding: 10px; - -.unified-search { - &__input-wrapper { - position: sticky; - // above search results - z-index: 2; - top: 0; - display: inline-flex; - flex-direction: column; - align-items: center; - width: 100%; - background-color: var(--color-main-background); +.header-menu { + display: flex; + align-items: center; + justify-content: center; - label[for="unified-search__input"] { - align-self: flex-start; - font-weight: bold; - font-size: 19px; - margin-left: 13px; - } - } - - &__form-input { - margin: 0 !important; - &:focus, - &:focus-visible, - &:active { - border-color: 2px solid var(--color-main-text) !important; - box-shadow: 0 0 0 2px var(--color-main-background) !important; - } - } - - &__input-row { + .unified-search__button { display: flex; - width: 100%; align-items: center; - } - - &__filters { - margin: $margin 0 $margin math.div($margin, 2); - padding-top: 5px; - ul { - display: inline-flex; - justify-content: space-between; - } - } - - &__form { - position: relative; - width: 100%; - margin: $margin 0; - - // Loading spinner - &::after { - right: $input-padding; - left: auto; - } - - &-input, - &-reset { - margin: math.div($input-padding, 2); - } - - &-input { - width: 100%; - height: $input-height; - padding: $input-padding; - - &, - &[placeholder], - &::placeholder { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - // Hide webkit clear search - &::-webkit-search-decoration, - &::-webkit-search-cancel-button, - &::-webkit-search-results-button, - &::-webkit-search-results-decoration { - -webkit-appearance: none; - } - } - - &-reset, &-submit { - position: absolute; - top: 0; - right: 4px; - width: $input-height - $input-padding; - height: $input-height - $input-padding; - min-height: 30px; - padding: 0; - opacity: .5; - border: none; - background-color: transparent; - margin-right: 0; - - &:hover, - &:focus, - &:active { - opacity: 1; - } - } - - &-submit { - right: 28px; + justify-content: center; + width: var(--header-height); + // height: var(--header-height); + margin: 0; + padding: 0; + cursor: pointer; + opacity: .85; + background-color: transparent; + border: none; + filter: none !important; + color: var(--color-primary-text) !important; + + &:hover { + background-color: transparent !important; } } +} - &__results { - &-header { - display: block; - margin: $margin; - margin-bottom: $margin - 4px; - margin-left: 13px; - color: var(--color-primary-element); - font-size: 19px; - font-weight: bold; - } - display: flex; - flex-direction: column; - gap: 4px; - } - - .unified-search__result-more::v-deep { - color: var(--color-text-maxcontrast); - } - - .empty-content { - margin: 10vh 0; - - ::v-deep .empty-content__title { - font-weight: normal; - font-size: var(--default-font-size); - text-align: center; - } +.unified-search-modal { + ::v-deep .modal-container { + height: 80%; } } - </style> diff --git a/core/src/views/GlobalSearchModal.vue b/core/src/views/UnifiedSearchModal.vue index 96fecd16245..1d6b138fdb3 100644 --- a/core/src/views/GlobalSearchModal.vue +++ b/core/src/views/UnifiedSearchModal.vue @@ -1,24 +1,23 @@ <template> - <NcModal id="global-search" - ref="globalSearchModal" + <NcModal id="unified-search" + ref="unifiedSearchModal" + :name="t('core', 'Unified search')" :show.sync="internalIsVisible" :clear-view-delay="0" @close="closeModal"> <CustomDateRangeModal :is-open="showDateRangeModal" - :class="'global-search__date-range'" + class="unified-search__date-range" @set:custom-date-range="setCustomDateRange" @update:is-open="showDateRangeModal = $event" /> - <!-- Global search form --> - <div ref="globalSearch" class="global-search-modal"> - <h2 class="global-search-modal__heading"> - {{ t('core', 'Unified search') }} - </h2> + <!-- 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="global-search-modal__filters"> + <div class="unified-search-modal__filters"> <NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen"> <template #icon> <ListBox :size="20" /> @@ -67,7 +66,7 @@ </template> </SearchableList> </div> - <div class="global-search-modal__filters-applied"> + <div class="unified-search-modal__filters-applied"> <FilterChip v-for="filter in filters" :key="filter.id" :text="filter.name ?? filter.text" @@ -85,14 +84,14 @@ </template> </FilterChip> </div> - <div v-if="noContentInfo.show" class="global-search-modal__no-content"> + <div v-if="noContentInfo.show" class="unified-search-modal__no-content"> <NcEmptyContent :name="noContentInfo.text"> <template #icon> <component :is="noContentInfo.icon" /> </template> </NcEmptyContent> </div> - <div v-for="providerResult in results" :key="providerResult.id" class="global-search-modal__results"> + <div v-for="providerResult in results" :key="providerResult.id" class="unified-search-modal__results"> <div class="results"> <div class="result-title"> <span>{{ providerResult.provider }}</span> @@ -116,7 +115,7 @@ </div> </div> </div> - <div v-if="supportFiltering()" class="global-search-modal__results"> + <div v-if="supportFiltering()" class="unified-search-modal__results"> <NcButton @click="closeModal"> {{ t('core', 'Filter in current view') }} <template #icon> @@ -132,10 +131,10 @@ import ArrowRight from 'vue-material-design-icons/ArrowRight.vue' import AccountGroup from 'vue-material-design-icons/AccountGroup.vue' import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue' -import CustomDateRangeModal from '../components/GlobalSearch/CustomDateRangeModal.vue' +import CustomDateRangeModal from '../components/UnifiedSearch/CustomDateRangeModal.vue' import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue' import FilterIcon from 'vue-material-design-icons/Filter.vue' -import FilterChip from '../components/GlobalSearch/SearchFilterChip.vue' +import FilterChip from '../components/UnifiedSearch/SearchFilterChip.vue' import ListBox from 'vue-material-design-icons/ListBox.vue' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' @@ -145,15 +144,15 @@ import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' import MagnifyIcon from 'vue-material-design-icons/Magnify.vue' -import SearchableList from '../components/GlobalSearch/SearchableList.vue' -import SearchResult from '../components/GlobalSearch/SearchResult.vue' +import SearchableList from '../components/UnifiedSearch/SearchableList.vue' +import SearchResult from '../components/UnifiedSearch/SearchResult.vue' import debounce from 'debounce' import { emit } from '@nextcloud/event-bus' -import { getProviders, search as globalSearch, getContacts } from '../services/GlobalSearchService.js' +import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js' export default { - name: 'GlobalSearchModal', + name: 'UnifiedSearchModal', components: { ArrowRight, AccountGroup, @@ -255,7 +254,7 @@ export default { this.searching = false return } - // Event should probably be refactored at some point to used nextcloud:global-search.search + // Event should probably be refactored at some point to used nextcloud:unified-search.search emit('nextcloud:unified-search.search', { query }) const newResults = [] const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers @@ -289,7 +288,7 @@ export default { params.limit = this.providerResultLimit } - const request = globalSearch(params).request + const request = unifiedSearch(params).request request().then((response) => { newResults.push({ @@ -300,7 +299,7 @@ export default { }) console.debug('New results', newResults) - console.debug('Global search results:', this.results) + console.debug('Unified search results:', this.results) this.updateResults(newResults) this.searching = false @@ -534,7 +533,7 @@ export default { </script> <style lang="scss" scoped> -.global-search-modal { +.unified-search-modal { padding: 10px 20px 10px 20px; height: 60%; |