Skip to content

Commit f755ee0

Browse files
committed
Files: Extend search to also cover tags
fixes #326 Signed-off-by: Marcel Klehr <[email protected]>
1 parent 4f55ba2 commit f755ee0

File tree

12 files changed

+253
-12
lines changed

12 files changed

+253
-12
lines changed

apps/systemtags/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
'OCA\\SystemTags\\Activity\\Setting' => $baseDir . '/../lib/Activity/Setting.php',
1313
'OCA\\SystemTags\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
1414
'OCA\\SystemTags\\Controller\\LastUsedController' => $baseDir . '/../lib/Controller/LastUsedController.php',
15+
'OCA\\SystemTags\\Search\\TagSearchProvider' => $baseDir . '/../lib/Search/TagSearchProvider.php',
1516
'OCA\\SystemTags\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
1617
);

apps/systemtags/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class ComposerStaticInitSystemTags
2727
'OCA\\SystemTags\\Activity\\Setting' => __DIR__ . '/..' . '/../lib/Activity/Setting.php',
2828
'OCA\\SystemTags\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
2929
'OCA\\SystemTags\\Controller\\LastUsedController' => __DIR__ . '/..' . '/../lib/Controller/LastUsedController.php',
30+
'OCA\\SystemTags\\Search\\TagSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TagSearchProvider.php',
3031
'OCA\\SystemTags\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
3132
);
3233

apps/systemtags/lib/AppInfo/Application.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626
namespace OCA\SystemTags\AppInfo;
2727

28+
use OCA\SystemTags\Search\TagSearchProvider;
2829
use OCA\SystemTags\Activity\Listener;
2930
use OCP\AppFramework\App;
3031
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -42,6 +43,7 @@ public function __construct() {
4243
}
4344

4445
public function register(IRegistrationContext $context): void {
46+
$context->registerSearchProvider(TagSearchProvider::class);
4547
}
4648

4749
public function boot(IBootContext $context): void {
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright 2020 Christoph Wurst <[email protected]>
7+
*
8+
* @author Christoph Wurst <[email protected]>
9+
* @author Joas Schilling <[email protected]>
10+
* @author John Molakvoæ <[email protected]>
11+
* @author Robin Appelman <[email protected]>
12+
* @author Roeland Jago Douma <[email protected]>
13+
* @author Marcel Klehr <[email protected]>
14+
*
15+
* @license GNU AGPL version 3 or any later version
16+
*
17+
* This program is free software: you can redistribute it and/or modify
18+
* it under the terms of the GNU Affero General Public License as
19+
* published by the Free Software Foundation, either version 3 of the
20+
* License, or (at your option) any later version.
21+
*
22+
* This program is distributed in the hope that it will be useful,
23+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
24+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25+
* GNU Affero General Public License for more details.
26+
*
27+
* You should have received a copy of the GNU Affero General Public License
28+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
29+
*
30+
*/
31+
namespace OCA\SystemTags\Search;
32+
33+
use OC\Files\Search\SearchBinaryOperator;
34+
use OC\Files\Search\SearchComparison;
35+
use OC\Files\Search\SearchOrder;
36+
use OC\Files\Search\SearchQuery;
37+
use OCP\SystemTag\ISystemTag;
38+
use OCP\SystemTag\ISystemTagManager;
39+
use OCP\SystemTag\ISystemTagObjectMapper;
40+
use OCP\Files\FileInfo;
41+
use OCP\Files\IMimeTypeDetector;
42+
use OCP\Files\IRootFolder;
43+
use OCP\Files\Search\ISearchComparison;
44+
use OCP\Files\Node;
45+
use OCP\Files\Search\ISearchOrder;
46+
use OCP\IL10N;
47+
use OCP\IURLGenerator;
48+
use OCP\IUser;
49+
use OCP\Search\IProvider;
50+
use OCP\Search\ISearchQuery;
51+
use OCP\Search\SearchResult;
52+
use OCP\Search\SearchResultEntry;
53+
use RecursiveArrayIterator;
54+
use RecursiveIteratorIterator;
55+
56+
class TagSearchProvider implements IProvider {
57+
58+
/** @var IL10N */
59+
private $l10n;
60+
61+
/** @var IURLGenerator */
62+
private $urlGenerator;
63+
64+
/** @var IMimeTypeDetector */
65+
private $mimeTypeDetector;
66+
67+
/** @var IRootFolder */
68+
private $rootFolder;
69+
private ISystemTagObjectMapper $objectMapper;
70+
private ISystemTagManager $tagManager;
71+
72+
public function __construct(
73+
IL10N $l10n,
74+
IURLGenerator $urlGenerator,
75+
IMimeTypeDetector $mimeTypeDetector,
76+
IRootFolder $rootFolder,
77+
ISystemTagObjectMapper $objectMapper,
78+
ISystemTagManager $tagManager
79+
) {
80+
$this->l10n = $l10n;
81+
$this->urlGenerator = $urlGenerator;
82+
$this->mimeTypeDetector = $mimeTypeDetector;
83+
$this->rootFolder = $rootFolder;
84+
$this->objectMapper = $objectMapper;
85+
$this->tagManager = $tagManager;
86+
}
87+
88+
/**
89+
* @inheritDoc
90+
*/
91+
public function getId(): string {
92+
return 'systemtags';
93+
}
94+
95+
/**
96+
* @inheritDoc
97+
*/
98+
public function getName(): string {
99+
return $this->l10n->t('Tags');
100+
}
101+
102+
/**
103+
* @inheritDoc
104+
*/
105+
public function getOrder(string $route, array $routeParameters): int {
106+
if ($route === 'files.View.index') {
107+
return -4;
108+
}
109+
return 6;
110+
}
111+
112+
/**
113+
* @inheritDoc
114+
*/
115+
public function search(IUser $user, ISearchQuery $query): SearchResult {
116+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
117+
$fileQuery = new SearchQuery(
118+
new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_OR, [
119+
new SearchComparison(ISearchComparison::COMPARE_LIKE, 'tagname', '%' . $query->getTerm() . '%'),
120+
new SearchComparison(ISearchComparison::COMPARE_LIKE, 'systemtag', '%' . $query->getTerm() . '%'),
121+
]),
122+
$query->getLimit(),
123+
(int)$query->getCursor(),
124+
$query->getSortOrder() === ISearchQuery::SORT_DATE_DESC ? [
125+
new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime'),
126+
] : [],
127+
$user
128+
);
129+
130+
// do search
131+
$searchResults = $userFolder->search($fileQuery);
132+
$resultIds = array_map(function(Node $node) {
133+
return $node->getId();
134+
}, $searchResults);
135+
$matchedTags = $this->objectMapper->getTagIdsForObjects($resultIds, 'files');
136+
$relevantTags = $this->tagManager->getTagsByIds(array_unique($this->flattenArray($matchedTags)));
137+
138+
// prepare direct tag results
139+
$tagResults = array_map(function(ISystemTag $tag) {
140+
$thumbnailUrl = '';
141+
$link = $this->urlGenerator->linkToRoute(
142+
'files.view.index'
143+
) . '?view=systemtagsfilter&tags='.$tag->getId();
144+
$searchResultEntry = new SearchResultEntry(
145+
$thumbnailUrl,
146+
$this->l10n->t('All tagged %s …', [$tag->getName()]),
147+
'',
148+
$this->urlGenerator->getAbsoluteURL($link),
149+
'icon-tag'
150+
);
151+
return $searchResultEntry;
152+
}, array_filter($relevantTags, function($tag) use ($query) {
153+
return $tag->isUserVisible() && strpos($tag->getName(), $query->getTerm()) !== false;
154+
}));
155+
156+
// prepare files results
157+
return SearchResult::paginated(
158+
$this->l10n->t('Tags'),
159+
array_map(function (Node $result) use ($userFolder, $matchedTags, $query) {
160+
// Generate thumbnail url
161+
$thumbnailUrl = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->getId()]);
162+
$path = $userFolder->getRelativePath($result->getPath());
163+
164+
// Use shortened link to centralize the various
165+
// files/folder url redirection in files.View.showFile
166+
$link = $this->urlGenerator->linkToRoute(
167+
'files.View.showFile',
168+
['fileid' => $result->getId()]
169+
);
170+
171+
$searchResultEntry = new SearchResultEntry(
172+
$thumbnailUrl,
173+
$result->getName(),
174+
$this->formatSubline($query, $matchedTags[$result->getId()]),
175+
$this->urlGenerator->getAbsoluteURL($link),
176+
$result->getMimetype() === FileInfo::MIMETYPE_FOLDER ? 'icon-folder' : $this->mimeTypeDetector->mimeTypeIcon($result->getMimetype())
177+
);
178+
$searchResultEntry->addAttribute('fileId', (string)$result->getId());
179+
$searchResultEntry->addAttribute('path', $path);
180+
return $searchResultEntry;
181+
}, $searchResults)
182+
+ $tagResults,
183+
$query->getCursor() + $query->getLimit()
184+
);
185+
}
186+
187+
/**
188+
* Format subline for tagged files: Show the first 3 tags
189+
*
190+
* @param $query
191+
* @param array $tagInfo
192+
* @return string
193+
*/
194+
private function formatSubline(ISearchQuery $query, array $tagInfo): string {
195+
/**
196+
* @var ISystemTag[]
197+
*/
198+
$tags = $this->tagManager->getTagsByIds($tagInfo);
199+
$tagNames = array_map(function($tag) {
200+
return $tag->getName();
201+
}, array_filter($tags, function($tag) {
202+
return $tag->isUserVisible();
203+
}));
204+
205+
// show the tag that you have searched for first
206+
usort($tagNames, function($tagName) use($query) {
207+
return strpos($tagName, $query->getTerm()) !== false? -1 : 1;
208+
});
209+
210+
return $this->l10n->t('tagged %s', [implode(', ', array_slice($tagNames, 0, 3))]);
211+
}
212+
213+
private function flattenArray($array) {
214+
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
215+
return iterator_to_array($it, true);
216+
}
217+
}

apps/systemtags/src/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
return this._fileList
3939
}
4040

41+
const tagsParam = (new URL(window.location.href)).searchParams.get('tags')
42+
const initialTags = tagsParam ? tagsParam.split(',').map(parseInt) : []
43+
4144
this._fileList = new OCA.SystemTags.FileList(
4245
$el,
4346
{
@@ -49,6 +52,7 @@
4952
// done if handling the event with the file list already
5053
// created.
5154
shown: true,
55+
systemTagIds: initialTags
5256
}
5357
)
5458

apps/systemtags/src/systemtagsfilelist.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
_initFilterField($container) {
102102
const self = this
103103
this.$filterField = $('<input type="hidden" name="tags"/>')
104+
this.$filterField.val(this._systemTagIds.join(','))
104105
$container.append(this.$filterField)
105106
this.$filterField.select2({
106107
placeholder: t('systemtags', 'Select tags to filter by'),
@@ -132,8 +133,8 @@
132133
tags.push(tag.toJSON())
133134
}
134135
})
135-
136136
callback(tags)
137+
self._onTagsChanged({ target: element })
137138
},
138139
})
139140
} else {

0 commit comments

Comments
 (0)