From f755ee08689a9400e1e9b2bc15ae116ae7483d5c Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 28 Apr 2021 19:07:15 +0200 Subject: Files: Extend search to also cover tags fixes #326 Signed-off-by: Marcel Klehr --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/systemtags/lib/AppInfo/Application.php | 2 + apps/systemtags/lib/Search/TagSearchProvider.php | 217 +++++++++++++++++++++ apps/systemtags/src/app.js | 4 + apps/systemtags/src/systemtagsfilelist.js | 3 +- 6 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 apps/systemtags/lib/Search/TagSearchProvider.php (limited to 'apps') diff --git a/apps/systemtags/composer/composer/autoload_classmap.php b/apps/systemtags/composer/composer/autoload_classmap.php index c2fb4daa824..604b7df1672 100644 --- a/apps/systemtags/composer/composer/autoload_classmap.php +++ b/apps/systemtags/composer/composer/autoload_classmap.php @@ -12,5 +12,6 @@ return array( 'OCA\\SystemTags\\Activity\\Setting' => $baseDir . '/../lib/Activity/Setting.php', 'OCA\\SystemTags\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\SystemTags\\Controller\\LastUsedController' => $baseDir . '/../lib/Controller/LastUsedController.php', + 'OCA\\SystemTags\\Search\\TagSearchProvider' => $baseDir . '/../lib/Search/TagSearchProvider.php', 'OCA\\SystemTags\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', ); diff --git a/apps/systemtags/composer/composer/autoload_static.php b/apps/systemtags/composer/composer/autoload_static.php index b679d3bf430..9c77f6d7a43 100644 --- a/apps/systemtags/composer/composer/autoload_static.php +++ b/apps/systemtags/composer/composer/autoload_static.php @@ -27,6 +27,7 @@ class ComposerStaticInitSystemTags 'OCA\\SystemTags\\Activity\\Setting' => __DIR__ . '/..' . '/../lib/Activity/Setting.php', 'OCA\\SystemTags\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\SystemTags\\Controller\\LastUsedController' => __DIR__ . '/..' . '/../lib/Controller/LastUsedController.php', + 'OCA\\SystemTags\\Search\\TagSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TagSearchProvider.php', 'OCA\\SystemTags\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', ); diff --git a/apps/systemtags/lib/AppInfo/Application.php b/apps/systemtags/lib/AppInfo/Application.php index cdc059d4a42..fc318aa2f1e 100644 --- a/apps/systemtags/lib/AppInfo/Application.php +++ b/apps/systemtags/lib/AppInfo/Application.php @@ -25,6 +25,7 @@ declare(strict_types=1); */ namespace OCA\SystemTags\AppInfo; +use OCA\SystemTags\Search\TagSearchProvider; use OCA\SystemTags\Activity\Listener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -42,6 +43,7 @@ class Application extends App implements IBootstrap { } public function register(IRegistrationContext $context): void { + $context->registerSearchProvider(TagSearchProvider::class); } public function boot(IBootContext $context): void { diff --git a/apps/systemtags/lib/Search/TagSearchProvider.php b/apps/systemtags/lib/Search/TagSearchProvider.php new file mode 100644 index 00000000000..7a7cb0b061c --- /dev/null +++ b/apps/systemtags/lib/Search/TagSearchProvider.php @@ -0,0 +1,217 @@ + + * + * @author Christoph Wurst + * @author Joas Schilling + * @author John Molakvoæ + * @author Robin Appelman + * @author Roeland Jago Douma + * @author Marcel Klehr + * + * @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 . + * + */ +namespace OCA\SystemTags\Search; + +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchOrder; +use OC\Files\Search\SearchQuery; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\Files\FileInfo; +use OCP\Files\IMimeTypeDetector; +use OCP\Files\IRootFolder; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Node; +use OCP\Files\Search\ISearchOrder; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Search\IProvider; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; +use RecursiveArrayIterator; +use RecursiveIteratorIterator; + +class TagSearchProvider implements IProvider { + + /** @var IL10N */ + private $l10n; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var IMimeTypeDetector */ + private $mimeTypeDetector; + + /** @var IRootFolder */ + private $rootFolder; + private ISystemTagObjectMapper $objectMapper; + private ISystemTagManager $tagManager; + + public function __construct( + IL10N $l10n, + IURLGenerator $urlGenerator, + IMimeTypeDetector $mimeTypeDetector, + IRootFolder $rootFolder, + ISystemTagObjectMapper $objectMapper, + ISystemTagManager $tagManager + ) { + $this->l10n = $l10n; + $this->urlGenerator = $urlGenerator; + $this->mimeTypeDetector = $mimeTypeDetector; + $this->rootFolder = $rootFolder; + $this->objectMapper = $objectMapper; + $this->tagManager = $tagManager; + } + + /** + * @inheritDoc + */ + public function getId(): string { + return 'systemtags'; + } + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l10n->t('Tags'); + } + + /** + * @inheritDoc + */ + public function getOrder(string $route, array $routeParameters): int { + if ($route === 'files.View.index') { + return -4; + } + return 6; + } + + /** + * @inheritDoc + */ + public function search(IUser $user, ISearchQuery $query): SearchResult { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $fileQuery = new SearchQuery( + new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_OR, [ + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'tagname', '%' . $query->getTerm() . '%'), + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'systemtag', '%' . $query->getTerm() . '%'), + ]), + $query->getLimit(), + (int)$query->getCursor(), + $query->getSortOrder() === ISearchQuery::SORT_DATE_DESC ? [ + new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime'), + ] : [], + $user + ); + + // do search + $searchResults = $userFolder->search($fileQuery); + $resultIds = array_map(function(Node $node) { + return $node->getId(); + }, $searchResults); + $matchedTags = $this->objectMapper->getTagIdsForObjects($resultIds, 'files'); + $relevantTags = $this->tagManager->getTagsByIds(array_unique($this->flattenArray($matchedTags))); + + // prepare direct tag results + $tagResults = array_map(function(ISystemTag $tag) { + $thumbnailUrl = ''; + $link = $this->urlGenerator->linkToRoute( + 'files.view.index' + ) . '?view=systemtagsfilter&tags='.$tag->getId(); + $searchResultEntry = new SearchResultEntry( + $thumbnailUrl, + $this->l10n->t('All tagged %s …', [$tag->getName()]), + '', + $this->urlGenerator->getAbsoluteURL($link), + 'icon-tag' + ); + return $searchResultEntry; + }, array_filter($relevantTags, function($tag) use ($query) { + return $tag->isUserVisible() && strpos($tag->getName(), $query->getTerm()) !== false; + })); + + // prepare files results + return SearchResult::paginated( + $this->l10n->t('Tags'), + array_map(function (Node $result) use ($userFolder, $matchedTags, $query) { + // Generate thumbnail url + $thumbnailUrl = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->getId()]); + $path = $userFolder->getRelativePath($result->getPath()); + + // Use shortened link to centralize the various + // files/folder url redirection in files.View.showFile + $link = $this->urlGenerator->linkToRoute( + 'files.View.showFile', + ['fileid' => $result->getId()] + ); + + $searchResultEntry = new SearchResultEntry( + $thumbnailUrl, + $result->getName(), + $this->formatSubline($query, $matchedTags[$result->getId()]), + $this->urlGenerator->getAbsoluteURL($link), + $result->getMimetype() === FileInfo::MIMETYPE_FOLDER ? 'icon-folder' : $this->mimeTypeDetector->mimeTypeIcon($result->getMimetype()) + ); + $searchResultEntry->addAttribute('fileId', (string)$result->getId()); + $searchResultEntry->addAttribute('path', $path); + return $searchResultEntry; + }, $searchResults) + + $tagResults, + $query->getCursor() + $query->getLimit() + ); + } + + /** + * Format subline for tagged files: Show the first 3 tags + * + * @param $query + * @param array $tagInfo + * @return string + */ + private function formatSubline(ISearchQuery $query, array $tagInfo): string { + /** + * @var ISystemTag[] + */ + $tags = $this->tagManager->getTagsByIds($tagInfo); + $tagNames = array_map(function($tag) { + return $tag->getName(); + }, array_filter($tags, function($tag) { + return $tag->isUserVisible(); + })); + + // show the tag that you have searched for first + usort($tagNames, function($tagName) use($query) { + return strpos($tagName, $query->getTerm()) !== false? -1 : 1; + }); + + return $this->l10n->t('tagged %s', [implode(', ', array_slice($tagNames, 0, 3))]); + } + + private function flattenArray($array) { + $it = new RecursiveIteratorIterator(new RecursiveArrayIterator($array)); + return iterator_to_array($it, true); + } +} diff --git a/apps/systemtags/src/app.js b/apps/systemtags/src/app.js index b5f75c0e7db..e7e5fea5372 100644 --- a/apps/systemtags/src/app.js +++ b/apps/systemtags/src/app.js @@ -38,6 +38,9 @@ return this._fileList } + const tagsParam = (new URL(window.location.href)).searchParams.get('tags') + const initialTags = tagsParam ? tagsParam.split(',').map(parseInt) : [] + this._fileList = new OCA.SystemTags.FileList( $el, { @@ -49,6 +52,7 @@ // done if handling the event with the file list already // created. shown: true, + systemTagIds: initialTags } ) diff --git a/apps/systemtags/src/systemtagsfilelist.js b/apps/systemtags/src/systemtagsfilelist.js index 468bee25b40..a87b5a96c3e 100644 --- a/apps/systemtags/src/systemtagsfilelist.js +++ b/apps/systemtags/src/systemtagsfilelist.js @@ -101,6 +101,7 @@ _initFilterField($container) { const self = this this.$filterField = $('') + this.$filterField.val(this._systemTagIds.join(',')) $container.append(this.$filterField) this.$filterField.select2({ placeholder: t('systemtags', 'Select tags to filter by'), @@ -132,8 +133,8 @@ tags.push(tag.toJSON()) } }) - callback(tags) + self._onTagsChanged({ target: element }) }, }) } else { -- cgit v1.2.3