diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-08-03 12:54:37 +0200 |
---|---|---|
committer | npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com> | 2020-08-03 11:26:03 +0000 |
commit | 1a1b3e20e470a945dd9f5fab1d99174b10cbb141 (patch) | |
tree | aacff8872bcfd47685e9a9fb3e5e3a423e498f59 /core/src | |
parent | 4987fe9a51f0b889d2b99428c967014d95bb13ae (diff) | |
download | nextcloud-server-1a1b3e20e470a945dd9f5fab1d99174b10cbb141.tar.gz nextcloud-server-1a1b3e20e470a945dd9f5fab1d99174b10cbb141.zip |
Fix unified search
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Signed-off-by: npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/components/HeaderMenu.vue | 206 | ||||
-rw-r--r-- | core/src/components/UnifiedSearch/SearchResult.vue | 211 | ||||
-rw-r--r-- | core/src/components/login/PasswordLessLoginForm.vue | 2 | ||||
-rw-r--r-- | core/src/services/UnifiedSearchService.js | 52 | ||||
-rw-r--r-- | core/src/services/WebAuthnAuthenticationService.js (renamed from core/src/service/WebAuthnAuthenticationService.js) | 0 | ||||
-rw-r--r-- | core/src/unified-search.js | 47 | ||||
-rw-r--r-- | core/src/views/UnifiedSearch.vue | 1 |
7 files changed, 517 insertions, 2 deletions
diff --git a/core/src/components/HeaderMenu.vue b/core/src/components/HeaderMenu.vue new file mode 100644 index 00000000000..2cc5b79d6dd --- /dev/null +++ b/core/src/components/HeaderMenu.vue @@ -0,0 +1,206 @@ + <!-- + - @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> + <div v-click-outside="closeMenu" :class="{ 'header-menu--opened': opened }" class="header-menu"> + <a class="header-menu__trigger" + href="#" + :aria-controls="`header-menu-${id}`" + :aria-expanded="opened" + aria-haspopup="true" + @click.prevent="toggleMenu"> + <slot name="trigger" /> + </a> + <div v-if="opened" + :id="`header-menu-${id}`" + class="header-menu__wrapper" + role="menu"> + <div class="header-menu__carret" /> + <div class="header-menu__content"> + <slot /> + </div> + </div> + </div> +</template> + +<script> +import { directive as ClickOutside } from 'v-click-outside' +import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' + +export default { + name: 'HeaderMenu', + + directives: { + ClickOutside, + }, + + props: { + id: { + type: String, + required: true, + }, + open: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + opened: this.open, + } + }, + + watch: { + open(newVal) { + this.opened = newVal + this.$nextTick(() => { + if (this.opened) { + this.openMenu() + } else { + this.closeMenu() + } + }) + }, + }, + + mounted() { + document.addEventListener('keydown', this.onKeyDown) + }, + + beforeMount() { + subscribe(`header-menu-${this.id}-close`, this.closeMenu) + subscribe(`header-menu-${this.id}-open`, this.openMenu) + }, + + beforeDestroy() { + unsubscribe(`header-menu-${this.id}-close`, this.closeMenu) + unsubscribe(`header-menu-${this.id}-open`, this.openMenu) + }, + + methods: { + /** + * Toggle the current menu open state + */ + toggleMenu() { + // Toggling current state + if (!this.opened) { + this.openMenu() + } else { + this.closeMenu() + } + }, + + /** + * Close the current menu + */ + closeMenu() { + if (!this.opened) { + return + } + + this.opened = false + this.$emit('close') + this.$emit('update:open', false) + emit(`header-menu-${this.id}-close`) + }, + + /** + * Open the current menu + */ + openMenu() { + if (this.opened) { + return + } + + this.opened = true + this.$emit('open') + this.$emit('update:open', true) + emit(`header-menu-${this.id}-open`) + }, + + onKeyDown(event) { + // If opened and escape pressed, close + if (event.key === 'Escape' && this.opened) { + event.preventDefault() + this.closeMenu() + } + }, + }, +} +</script> + +<style lang="scss" scoped> +.header-menu { + &__trigger { + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 100%; + margin: 0; + padding: 0; + cursor: pointer; + opacity: .6; + } + + &--opened &__trigger, + &__trigger:hover, + &__trigger:focus, + &__trigger:active { + opacity: 1; + } + + &__wrapper { + position: absolute; + z-index: 2000; + top: 50px; + right: 5px; + box-sizing: border-box; + margin: 0; + border-radius: 0 0 var(--border-radius) var(--border-radius); + background-color: var(--color-main-background); + + filter: drop-shadow(0 1px 5px var(--color-box-shadow)); + } + + &__carret { + position: absolute; + right: 10px; + bottom: 100%; + width: 0; + height: 0; + content: ' '; + pointer-events: none; + border: 10px solid transparent; + border-bottom-color: var(--color-main-background); + } + + &__content { + overflow: auto; + width: 350px; + max-width: 350px; + min-height: calc(44px * 1.5); + max-height: calc(100vh - 50px * 2); + } +} + +</style> diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue new file mode 100644 index 00000000000..832770c9abe --- /dev/null +++ b/core/src/components/UnifiedSearch/SearchResult.vue @@ -0,0 +1,211 @@ +<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, + [iconClass]: true + }" + role="img"> + <img v-if="hasValidThumbnail" + :src="thumbnailUrl" + :alt="t('core', 'Thumbnail for {result}', {result: title})" + @error="onError" + @load="onLoad"> + </div> + + <!-- Title and sub-title --> + <span class="unified-search__result-content"> + <h3 class="unified-search__result-line-one"> + <Highlight :text="title" :search="query" /> + </h3> + <h4 v-if="subline" class="unified-search__result-line-two">{{ subline }}</h4> + </span> + </a> +</template> + +<script> +import Highlight from '@nextcloud/vue/dist/Components/Highlight' + +export default { + name: 'SearchResult', + + components: { + Highlight, + }, + + props: { + thumbnailUrl: { + type: String, + default: null, + }, + title: { + type: String, + required: true, + }, + subline: { + type: String, + default: null, + }, + resourceUrl: { + type: String, + default: null, + }, + iconClass: { + 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, + } + }, + + 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> +$clickable-area: 44px; +$margin: 10px; + +.unified-search__result { + display: flex; + height: $clickable-area; + padding: $margin; + border-bottom: 1px solid var(--color-border); + + // Load more entry, + &:last-child { + border-bottom: none; + } + + &--focused, + &:active, + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + * { + cursor: pointer; + } + + &-icon { + overflow: hidden; + width: $clickable-area; + height: $clickable-area; + border-radius: var(--border-radius); + background-position: center center; + background-size: 32px; + &--rounded { + border-radius: $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: 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: 14px; + } +} + +</style> diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue index 0cd7cb81cfe..df774599f92 100644 --- a/core/src/components/login/PasswordLessLoginForm.vue +++ b/core/src/components/login/PasswordLessLoginForm.vue @@ -41,7 +41,7 @@ import { startAuthentication, finishAuthentication, -} from '../../service/WebAuthnAuthenticationService' +} from '../../services/WebAuthnAuthenticationService' import LoginButton from './LoginButton' class NoValidCredentials extends Error { diff --git a/core/src/services/UnifiedSearchService.js b/core/src/services/UnifiedSearchService.js new file mode 100644 index 00000000000..2e63f19767d --- /dev/null +++ b/core/src/services/UnifiedSearchService.js @@ -0,0 +1,52 @@ +/** + * @copyright 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/>. + */ + +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' +import axios from '@nextcloud/axios' + +export const defaultLimit = loadState('unified-search', 'limit-default') + +/** + * Get the list of available search providers + */ +export async function getTypes() { + try { + const { data } = await axios.get(generateUrl('/search/providers')) + if (Array.isArray(data) && data.length > 0) { + return data + } + } catch (error) { + console.error(error) + } + return [] +} + +/** + * Get the list of available search providers + * + * @param {string} type the type to search + * @param {string} query the search + * @returns {Promise} + */ +export function search(type, query) { + return axios.get(generateUrl(`/search/providers/${type}/search?term=${query}`)) +} diff --git a/core/src/service/WebAuthnAuthenticationService.js b/core/src/services/WebAuthnAuthenticationService.js index 91f19177066..91f19177066 100644 --- a/core/src/service/WebAuthnAuthenticationService.js +++ b/core/src/services/WebAuthnAuthenticationService.js diff --git a/core/src/unified-search.js b/core/src/unified-search.js new file mode 100644 index 00000000000..ba975d78564 --- /dev/null +++ b/core/src/unified-search.js @@ -0,0 +1,47 @@ +/** + * @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/>. + */ + +import { getRequestToken } from '@nextcloud/auth' +import { generateFilePath } from '@nextcloud/router' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import Vue from 'vue' + +import UnifiedSearch from './views/UnifiedSearch.vue' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = btoa(getRequestToken()) + +// eslint-disable-next-line camelcase +__webpack_public_path__ = generateFilePath('core', '', 'js/') + +Vue.mixin({ + methods: { + t, + n, + }, +}) + +export default new Vue({ + el: '#unified-search', + // eslint-disable-next-line vue/match-component-file-name + name: 'UnifiedSearchRoot', + render: h => h(UnifiedSearch), +}) diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue index 099ed7c325f..4535e1fde5a 100644 --- a/core/src/views/UnifiedSearch.vue +++ b/core/src/views/UnifiedSearch.vue @@ -451,7 +451,6 @@ export default { const entry = event.target const results = this.getResultsList() const index = [...results].findIndex(search => search === entry) - console.info(entry, index) if (index > -1) { // let's not use focusIndex as the entry is already focused this.focused = index |