Fix unified search
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> Signed-off-by: npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>
This commit is contained in:
parent
4987fe9a51
commit
1a1b3e20e4
|
@ -28,6 +28,7 @@ namespace OCA\Comments\Search;
|
||||||
use OCP\IL10N;
|
use OCP\IL10N;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
use OCP\IUser;
|
use OCP\IUser;
|
||||||
|
use OCP\IUserManager;
|
||||||
use OCP\Search\IProvider;
|
use OCP\Search\IProvider;
|
||||||
use OCP\Search\ISearchQuery;
|
use OCP\Search\ISearchQuery;
|
||||||
use OCP\Search\SearchResult;
|
use OCP\Search\SearchResult;
|
||||||
|
@ -36,6 +37,9 @@ use function pathinfo;
|
||||||
|
|
||||||
class Provider implements IProvider {
|
class Provider implements IProvider {
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
/** @var IL10N */
|
/** @var IL10N */
|
||||||
private $l10n;
|
private $l10n;
|
||||||
|
|
||||||
|
@ -45,9 +49,11 @@ class Provider implements IProvider {
|
||||||
/** @var LegacyProvider */
|
/** @var LegacyProvider */
|
||||||
private $legacyProvider;
|
private $legacyProvider;
|
||||||
|
|
||||||
public function __construct(IL10N $l10n,
|
public function __construct(IUserManager $userManager,
|
||||||
|
IL10N $l10n,
|
||||||
IURLGenerator $urlGenerator,
|
IURLGenerator $urlGenerator,
|
||||||
LegacyProvider $legacyProvider) {
|
LegacyProvider $legacyProvider) {
|
||||||
|
$this->userManager = $userManager;
|
||||||
$this->l10n = $l10n;
|
$this->l10n = $l10n;
|
||||||
$this->urlGenerator = $urlGenerator;
|
$this->urlGenerator = $urlGenerator;
|
||||||
$this->legacyProvider = $legacyProvider;
|
$this->legacyProvider = $legacyProvider;
|
||||||
|
@ -57,23 +63,30 @@ class Provider implements IProvider {
|
||||||
return 'comments';
|
return 'comments';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return $this->l10n->t('Comments');
|
||||||
|
}
|
||||||
|
|
||||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||||
return SearchResult::complete(
|
return SearchResult::complete(
|
||||||
$this->l10n->t('Comments'),
|
$this->l10n->t('Comments'),
|
||||||
array_map(function (Result $result) {
|
array_map(function (Result $result) {
|
||||||
$path = $result->path;
|
$path = $result->path;
|
||||||
$pathInfo = pathinfo($path);
|
$pathInfo = pathinfo($path);
|
||||||
|
$isUser = $this->userManager->userExists($result->authorId);
|
||||||
|
$avatarUrl = $isUser
|
||||||
|
? $this->urlGenerator->linkToRoute('core.avatar.getAvatar', ['userId' => $result->authorId, 'size' => 42])
|
||||||
|
: $this->urlGenerator->linkToRoute('core.GuestAvatar.getAvatar', ['guestName' => $result->authorId, 'size' => 42]);
|
||||||
return new CommentsSearchResultEntry(
|
return new CommentsSearchResultEntry(
|
||||||
$this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->id]),
|
$avatarUrl,
|
||||||
$result->name,
|
$result->name,
|
||||||
$path,
|
$path,
|
||||||
$this->urlGenerator->linkToRoute(
|
$this->urlGenerator->linkToRoute('files.view.index',[
|
||||||
'files.view.index',
|
|
||||||
[
|
|
||||||
'dir' => $pathInfo['dirname'],
|
'dir' => $pathInfo['dirname'],
|
||||||
'scrollto' => $pathInfo['basename'],
|
'scrollto' => $pathInfo['basename'],
|
||||||
]
|
]),
|
||||||
)
|
'',
|
||||||
|
true
|
||||||
);
|
);
|
||||||
}, $this->legacyProvider->search($query->getTerm()))
|
}, $this->legacyProvider->search($query->getTerm()))
|
||||||
);
|
);
|
||||||
|
|
|
@ -57,17 +57,43 @@ class FilesSearchProvider implements IProvider {
|
||||||
return 'files';
|
return 'files';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return $this->l10n->t('Files');
|
||||||
|
}
|
||||||
|
|
||||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||||
return SearchResult::complete(
|
return SearchResult::complete(
|
||||||
$this->l10n->t('Files'),
|
$this->l10n->t('Files'),
|
||||||
array_map(function (FileResult $result) {
|
array_map(function (FileResult $result) {
|
||||||
|
// Generate thumbnail url
|
||||||
|
$thumbnailUrl = $result->type === 'folder'
|
||||||
|
? ''
|
||||||
|
: $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->id]);
|
||||||
|
|
||||||
return new FilesSearchResultEntry(
|
return new FilesSearchResultEntry(
|
||||||
$this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->id]),
|
$thumbnailUrl,
|
||||||
$result->name,
|
$result->name,
|
||||||
$result->path,
|
$this->formatSubline($result),
|
||||||
$result->link
|
$result->link,
|
||||||
|
$result->type === 'folder' ? 'icon-folder' : 'icon-filetype-file'
|
||||||
);
|
);
|
||||||
}, $this->fileSearch->search($query->getTerm()))
|
}, $this->fileSearch->search($query->getTerm()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format subline for files
|
||||||
|
*
|
||||||
|
* @param FileResult $result
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function formatSubline($result): string {
|
||||||
|
// Do not show the location if the file is in root
|
||||||
|
if ($result->path === '/' . $result->name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = ltrim(dirname($result->path), '/');
|
||||||
|
return $this->l10n->t('in %s', [$path]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,8 @@ class FilesSearchResultEntry extends ASearchResultEntry {
|
||||||
public function __construct(string $thumbnailUrl,
|
public function __construct(string $thumbnailUrl,
|
||||||
string $filename,
|
string $filename,
|
||||||
string $path,
|
string $path,
|
||||||
string $url) {
|
string $url,
|
||||||
parent::__construct($thumbnailUrl, $filename, $path, $url);
|
string $icon) {
|
||||||
|
parent::__construct($thumbnailUrl, $filename, $path, $url, $icon, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ class UnifiedSearchController extends Controller {
|
||||||
?int $sortOrder = null,
|
?int $sortOrder = null,
|
||||||
?int $limit = null,
|
?int $limit = null,
|
||||||
$cursor = null): JSONResponse {
|
$cursor = null): JSONResponse {
|
||||||
if (empty($term)) {
|
if (empty(trim($term))) {
|
||||||
return new JSONResponse(null, Http::STATUS_BAD_REQUEST);
|
return new JSONResponse(null, Http::STATUS_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,4 +46,6 @@
|
||||||
|
|
||||||
--animation-quick: $animation-quick;
|
--animation-quick: $animation-quick;
|
||||||
--animation-slow: $animation-slow;
|
--animation-slow: $animation-slow;
|
||||||
|
|
||||||
|
--header-height: $header-height;
|
||||||
}
|
}
|
||||||
|
|
|
@ -384,20 +384,20 @@ audio, canvas, embed, iframe, img, input, object, video {
|
||||||
|
|
||||||
.icon-file,
|
.icon-file,
|
||||||
.icon-filetype-text {
|
.icon-filetype-text {
|
||||||
@include icon-color('text', 'filetypes', $color-black, 1, true);
|
@include icon-color('text', 'filetypes', #969696, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-filetype-file {
|
.icon-filetype-file {
|
||||||
@include icon-color('file', 'filetypes', $color-black, 1, true);
|
@include icon-color('file', 'filetypes', #969696, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include icon-black-white('folder', 'filetypes', 1, true);
|
@include icon-black-white('folder', 'filetypes', 1, true);
|
||||||
.icon-filetype-folder {
|
.icon-filetype-folder {
|
||||||
@include icon-color('folder', 'filetypes', $color-black, 1, true);
|
@include icon-color('folder', 'filetypes', $color-primary, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-filetype-folder-drag-accept {
|
.icon-filetype-folder-drag-accept {
|
||||||
@include icon-color('folder-drag-accept', 'filetypes', $color-black, 1, true);
|
@include icon-color('folder-drag-accept', 'filetypes', $color-primary, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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>
|
|
@ -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>
|
|
@ -41,7 +41,7 @@
|
||||||
import {
|
import {
|
||||||
startAuthentication,
|
startAuthentication,
|
||||||
finishAuthentication,
|
finishAuthentication,
|
||||||
} from '../../service/WebAuthnAuthenticationService'
|
} from '../../services/WebAuthnAuthenticationService'
|
||||||
import LoginButton from './LoginButton'
|
import LoginButton from './LoginButton'
|
||||||
|
|
||||||
class NoValidCredentials extends Error {
|
class NoValidCredentials extends Error {
|
||||||
|
|
|
@ -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}`))
|
||||||
|
}
|
|
@ -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),
|
||||||
|
})
|
|
@ -451,7 +451,6 @@ export default {
|
||||||
const entry = event.target
|
const entry = event.target
|
||||||
const results = this.getResultsList()
|
const results = this.getResultsList()
|
||||||
const index = [...results].findIndex(search => search === entry)
|
const index = [...results].findIndex(search => search === entry)
|
||||||
console.info(entry, index)
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
// let's not use focusIndex as the entry is already focused
|
// let's not use focusIndex as the entry is already focused
|
||||||
this.focused = index
|
this.focused = index
|
||||||
|
|
|
@ -102,15 +102,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<form class="searchbox" action="#" method="post" role="search" novalidate>
|
<div id="unified-search"></div>
|
||||||
<label for="searchbox" class="hidden-visually">
|
|
||||||
<?php p($l->t('Search'));?>
|
|
||||||
</label>
|
|
||||||
<input id="searchbox" type="search" name="query"
|
|
||||||
value="" required class="hidden icon-search-white icon-search-force-white"
|
|
||||||
autocomplete="off">
|
|
||||||
<button class="icon-close-white" type="reset"><span class="hidden-visually"><?php p($l->t('Reset search'));?></span></button>
|
|
||||||
</form>
|
|
||||||
<div id="contactsmenu">
|
<div id="contactsmenu">
|
||||||
<div class="icon-contacts menutoggle" tabindex="0" role="button"
|
<div class="icon-contacts menutoggle" tabindex="0" role="button"
|
||||||
aria-haspopup="true" aria-controls="contactsmenu-menu" aria-expanded="false">
|
aria-haspopup="true" aria-controls="contactsmenu-menu" aria-expanded="false">
|
||||||
|
|
|
@ -4,19 +4,20 @@ const webpack = require('webpack')
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
entry: {
|
entry: {
|
||||||
|
files_client: path.join(__dirname, 'src/files/client.js'),
|
||||||
|
files_fileinfo: path.join(__dirname, 'src/files/fileinfo.js'),
|
||||||
|
files_iedavclient: path.join(__dirname, 'src/files/iedavclient.js'),
|
||||||
|
install: path.join(__dirname, 'src/install.js'),
|
||||||
login: path.join(__dirname, 'src/login.js'),
|
login: path.join(__dirname, 'src/login.js'),
|
||||||
main: path.join(__dirname, 'src/main.js'),
|
main: path.join(__dirname, 'src/main.js'),
|
||||||
maintenance: path.join(__dirname, 'src/maintenance.js'),
|
maintenance: path.join(__dirname, 'src/maintenance.js'),
|
||||||
recommendedapps: path.join(__dirname, 'src/recommendedapps.js'),
|
recommendedapps: path.join(__dirname, 'src/recommendedapps.js'),
|
||||||
install: path.join(__dirname, 'src/install.js'),
|
'unified-search': path.join(__dirname, 'src/unified-search.js'),
|
||||||
files_client: path.join(__dirname, 'src/files/client.js'),
|
|
||||||
files_fileinfo: path.join(__dirname, 'src/files/fileinfo.js'),
|
|
||||||
files_iedavclient: path.join(__dirname, 'src/files/iedavclient.js')
|
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
path: path.resolve(__dirname, 'js/dist'),
|
path: path.resolve(__dirname, 'js/dist'),
|
||||||
jsonpFunction: 'webpackJsonpCore'
|
jsonpFunction: 'webpackJsonpCore',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -26,25 +27,25 @@ module.exports = [
|
||||||
options: {
|
options: {
|
||||||
type: 'commonjs',
|
type: 'commonjs',
|
||||||
exports: 'dav',
|
exports: 'dav',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
'_': "underscore",
|
'_': 'underscore',
|
||||||
$: "jquery",
|
$: 'jquery',
|
||||||
jQuery: "jquery"
|
jQuery: 'jquery',
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: {
|
entry: {
|
||||||
systemtags: path.resolve(__dirname, 'src/systemtags/merged-systemtags.js')
|
systemtags: path.resolve(__dirname, 'src/systemtags/merged-systemtags.js'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
path: path.resolve(__dirname, 'js/dist')
|
path: path.resolve(__dirname, 'js/dist'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -106,9 +106,9 @@ class SearchComposer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of all provider IDs for the consecutive calls to `search`
|
* Get a list of all provider IDs & Names for the consecutive calls to `search`
|
||||||
*
|
*
|
||||||
* @return string[]
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getProviders(): array {
|
public function getProviders(): array {
|
||||||
$this->loadLazyProviders();
|
$this->loadLazyProviders();
|
||||||
|
@ -118,7 +118,10 @@ class SearchComposer {
|
||||||
*/
|
*/
|
||||||
return array_values(
|
return array_values(
|
||||||
array_map(function (IProvider $provider) {
|
array_map(function (IProvider $provider) {
|
||||||
return $provider->getId();
|
return [
|
||||||
|
'id' => $provider->getId(),
|
||||||
|
'name' => $provider->getName()
|
||||||
|
];
|
||||||
}, $this->providers));
|
}, $this->providers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace OC\Search;
|
||||||
use OCP\Search\ISearchQuery;
|
use OCP\Search\ISearchQuery;
|
||||||
|
|
||||||
class SearchQuery implements ISearchQuery {
|
class SearchQuery implements ISearchQuery {
|
||||||
public const LIMIT_DEFAULT = 20;
|
public const LIMIT_DEFAULT = 5;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $term;
|
private $term;
|
||||||
|
|
|
@ -44,34 +44,40 @@
|
||||||
|
|
||||||
namespace OC;
|
namespace OC;
|
||||||
|
|
||||||
use OC\AppFramework\Http\Request;
|
use OC\Search\SearchQuery;
|
||||||
use OC\Template\JSCombiner;
|
use OC\Template\JSCombiner;
|
||||||
use OC\Template\JSConfigHelper;
|
use OC\Template\JSConfigHelper;
|
||||||
use OC\Template\SCSSCacher;
|
use OC\Template\SCSSCacher;
|
||||||
use OCP\AppFramework\Http\TemplateResponse;
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
use OCP\Defaults;
|
use OCP\Defaults;
|
||||||
|
use OCP\IConfig;
|
||||||
use OCP\IInitialStateService;
|
use OCP\IInitialStateService;
|
||||||
use OCP\Support\Subscription\IRegistry;
|
use OCP\Support\Subscription\IRegistry;
|
||||||
|
use OCP\Util;
|
||||||
|
|
||||||
class TemplateLayout extends \OC_Template {
|
class TemplateLayout extends \OC_Template {
|
||||||
private static $versionHash = '';
|
private static $versionHash = '';
|
||||||
|
|
||||||
/**
|
/** @var IConfig */
|
||||||
* @var \OCP\IConfig
|
|
||||||
*/
|
|
||||||
private $config;
|
private $config;
|
||||||
|
|
||||||
|
/** @var IInitialStateService */
|
||||||
|
private $initialState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $renderAs
|
* @param string $renderAs
|
||||||
* @param string $appId application id
|
* @param string $appId application id
|
||||||
*/
|
*/
|
||||||
public function __construct($renderAs, $appId = '') {
|
public function __construct($renderAs, $appId = '') {
|
||||||
|
|
||||||
// yes - should be injected ....
|
/** @var IConfig */
|
||||||
$this->config = \OC::$server->getConfig();
|
$this->config = \OC::$server->get(IConfig::class);
|
||||||
|
|
||||||
if (\OCP\Util::isIE()) {
|
/** @var IInitialStateService */
|
||||||
\OC_Util::addStyle('ie');
|
$this->initialState = \OC::$server->get(InitialStateService::class);
|
||||||
|
|
||||||
|
if (Util::isIE()) {
|
||||||
|
Util::addStyle('ie');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decide which page we show
|
// Decide which page we show
|
||||||
|
@ -83,6 +89,9 @@ class TemplateLayout extends \OC_Template {
|
||||||
$this->assign('bodyid', 'body-user');
|
$this->assign('bodyid', 'body-user');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->initialState->provideInitialState('unified-search', 'limit-default', SearchQuery::LIMIT_DEFAULT);
|
||||||
|
Util::addScript('dist/unified-search', null, true);
|
||||||
|
|
||||||
// Add navigation entry
|
// Add navigation entry
|
||||||
$this->assign('application', '');
|
$this->assign('application', '');
|
||||||
$this->assign('appid', $appId);
|
$this->assign('appid', $appId);
|
||||||
|
@ -241,9 +250,7 @@ class TemplateLayout extends \OC_Template {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var InitialStateService $initialState */
|
$this->assign('initialStates', $this->initialState->getInitialStates());
|
||||||
$initialState = \OC::$server->query(InitialStateService::class);
|
|
||||||
$this->assign('initialStates', $initialState->getInitialStates());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -68,22 +68,40 @@ abstract class ASearchResultEntry implements JsonSerializable {
|
||||||
*/
|
*/
|
||||||
protected $resourceUrl;
|
protected $resourceUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @since 20.0.0
|
||||||
|
*/
|
||||||
|
protected $iconClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var boolean
|
||||||
|
* @since 20.0.0
|
||||||
|
*/
|
||||||
|
protected $rounded;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $thumbnailUrl a relative or absolute URL to the thumbnail or icon of the entry
|
* @param string $thumbnailUrl a relative or absolute URL to the thumbnail or icon of the entry
|
||||||
* @param string $title a main title of the entry
|
* @param string $title a main title of the entry
|
||||||
* @param string $subline the secondary line of the entry
|
* @param string $subline the secondary line of the entry
|
||||||
* @param string $resourceUrl the URL where the user can find the detail, like a deep link inside the app
|
* @param string $resourceUrl the URL where the user can find the detail, like a deep link inside the app
|
||||||
|
* @param string $iconClass the icon class fallback
|
||||||
|
* @param boolean $rounded is the thumbnail rounded
|
||||||
*
|
*
|
||||||
* @since 20.0.0
|
* @since 20.0.0
|
||||||
*/
|
*/
|
||||||
public function __construct(string $thumbnailUrl,
|
public function __construct(string $thumbnailUrl,
|
||||||
string $title,
|
string $title,
|
||||||
string $subline,
|
string $subline,
|
||||||
string $resourceUrl) {
|
string $resourceUrl,
|
||||||
|
string $iconClass = '',
|
||||||
|
bool $rounded = false) {
|
||||||
$this->thumbnailUrl = $thumbnailUrl;
|
$this->thumbnailUrl = $thumbnailUrl;
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
$this->subline = $subline;
|
$this->subline = $subline;
|
||||||
$this->resourceUrl = $resourceUrl;
|
$this->resourceUrl = $resourceUrl;
|
||||||
|
$this->iconClass = $iconClass;
|
||||||
|
$this->rounded = $rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,6 +115,8 @@ abstract class ASearchResultEntry implements JsonSerializable {
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'subline' => $this->subline,
|
'subline' => $this->subline,
|
||||||
'resourceUrl' => $this->resourceUrl,
|
'resourceUrl' => $this->resourceUrl,
|
||||||
|
'iconClass' => $this->iconClass,
|
||||||
|
'rounded' => $this->rounded,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,17 @@ interface IProvider {
|
||||||
*/
|
*/
|
||||||
public function getId(): string;
|
public function getId(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the translated name of this search provider
|
||||||
|
*
|
||||||
|
* Example: 'Mail', 'Contacts'...
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @since 20.0.0
|
||||||
|
*/
|
||||||
|
public function getName(): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find matching search entries in an app
|
* Find matching search entries in an app
|
||||||
*
|
*
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
|
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
|
||||||
"underscore": "^1.10.2",
|
"underscore": "^1.10.2",
|
||||||
"url-search-params-polyfill": "^8.0.0",
|
"url-search-params-polyfill": "^8.0.0",
|
||||||
|
"v-click-outside": "^3.0.1",
|
||||||
"v-tooltip": "^2.0.3",
|
"v-tooltip": "^2.0.3",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-click-outside": "^1.1.0",
|
"vue-click-outside": "^1.1.0",
|
||||||
|
|
Loading…
Reference in New Issue