summaryrefslogtreecommitdiffstats
path: root/core/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-08-03 12:54:37 +0200
committernpmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>2020-08-03 11:26:03 +0000
commit1a1b3e20e470a945dd9f5fab1d99174b10cbb141 (patch)
treeaacff8872bcfd47685e9a9fb3e5e3a423e498f59 /core/src
parent4987fe9a51f0b889d2b99428c967014d95bb13ae (diff)
downloadnextcloud-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.vue206
-rw-r--r--core/src/components/UnifiedSearch/SearchResult.vue211
-rw-r--r--core/src/components/login/PasswordLessLoginForm.vue2
-rw-r--r--core/src/services/UnifiedSearchService.js52
-rw-r--r--core/src/services/WebAuthnAuthenticationService.js (renamed from core/src/service/WebAuthnAuthenticationService.js)0
-rw-r--r--core/src/unified-search.js47
-rw-r--r--core/src/views/UnifiedSearch.vue1
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