Skip to content

Commit 0dd4d80

Browse files
committed
feat: unified search
Signed-off-by: Crisciany Souza <[email protected]>
1 parent 6d6fcf9 commit 0dd4d80

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use OCA\Libresign\Middleware\GlobalInjectionMiddleware;
2828
use OCA\Libresign\Middleware\InjectionMiddleware;
2929
use OCA\Libresign\Notification\Notifier;
30+
use OCA\Libresign\Search\FileSearchProvider;
3031
use OCP\AppFramework\App;
3132
use OCP\AppFramework\Bootstrap\IBootContext;
3233
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -64,6 +65,8 @@ public function register(IRegistrationContext $context): void {
6465

6566
$context->registerNotifierService(Notifier::class);
6667

68+
$context->registerSearchProvider(FileSearchProvider::class);
69+
6770
$context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
6871
$context->registerEventListener(BeforeNodeDeletedEvent::class, BeforeNodeDeletedListener::class);
6972
$context->registerEventListener(CacheEntryRemovedEvent::class, BeforeNodeDeletedListener::class);

lib/Search/FileSearchProvider.php

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Libresign\Search;
11+
12+
use OC\Files\FileInfo;
13+
use OCA\Libresign\AppInfo\Application;
14+
use OCA\Libresign\Db\File;
15+
use OCP\App\IAppManager;
16+
use OCP\Files\IMimeTypeDetector;
17+
use OCP\Files\IRootFolder;
18+
use OCP\IDBConnection;
19+
use OCP\IL10N;
20+
use OCP\IURLGenerator;
21+
use OCP\IUser;
22+
use OCP\Search\IProvider;
23+
use OCP\Search\ISearchQuery;
24+
use OCP\Search\SearchResult;
25+
use OCP\Search\SearchResultEntry;
26+
27+
class FileSearchProvider implements IProvider {
28+
public function __construct(
29+
private IL10N $l10n,
30+
private IURLGenerator $urlGenerator,
31+
private IRootFolder $rootFolder,
32+
private IAppManager $appManager,
33+
private IDBConnection $db,
34+
private IMimeTypeDetector $mimeTypeDetector,
35+
) {
36+
}
37+
38+
#[\Override]
39+
public function getId(): string {
40+
return 'libresign_files';
41+
}
42+
43+
#[\Override]
44+
public function getName(): string {
45+
return $this->l10n->t('LibreSign documents');
46+
}
47+
48+
#[\Override]
49+
public function getOrder(string $route, array $routeParameters): int {
50+
if (strpos($route, Application::APP_ID . '.') === 0) {
51+
return 0;
52+
}
53+
return 10;
54+
}
55+
56+
#[\Override]
57+
public function search(IUser $user, ISearchQuery $query): SearchResult {
58+
if (!$this->appManager->isEnabledForUser(Application::APP_ID, $user)) {
59+
return SearchResult::complete($this->l10n->t('LibreSign documents'), []);
60+
}
61+
62+
$term = $query->getTerm();
63+
$limit = $query->getLimit();
64+
$offset = $query->getCursor();
65+
66+
try {
67+
$files = $this->searchFiles($user, $term, $limit, (int)$offset);
68+
} catch (\Exception $e) {
69+
return SearchResult::complete($this->l10n->t('LibreSign documents'), []);
70+
}
71+
72+
$results = array_map(function (File $file) use ($user) {
73+
return $this->formatResult($file, $user);
74+
}, $files);
75+
76+
return SearchResult::paginated(
77+
$this->l10n->t('LibreSign documents'),
78+
$results,
79+
$offset + $limit
80+
);
81+
}
82+
83+
/**
84+
* Search for LibreSign files matching the search term
85+
*
86+
* @param IUser $user Current user
87+
* @param string $term Search term
88+
* @param int $limit Maximum number of results
89+
* @param int $offset Offset for pagination
90+
* @return File[] Array of File entities
91+
*/
92+
private function searchFiles(IUser $user, string $term, int $limit, int $offset): array {
93+
$qb = $this->db->getQueryBuilder();
94+
95+
$qb->select('*')
96+
->from('libresign_file')
97+
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())))
98+
->setMaxResults($limit)
99+
->setFirstResult($offset);
100+
101+
if (!empty($term)) {
102+
$qb->andWhere(
103+
$qb->expr()->like('name', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))
104+
);
105+
}
106+
107+
$qb->orderBy('created_at', 'DESC');
108+
109+
$result = $qb->executeQuery();
110+
$files = [];
111+
112+
while ($row = $result->fetch()) {
113+
try {
114+
$file = new File();
115+
$file->setId((int)$row['id']);
116+
$file->setUserId($row['user_id']);
117+
$file->setNodeId((int)($row['node_id'] ?? 0));
118+
$file->setName($row['name'] ?? '');
119+
$file->setStatus((int)($row['status'] ?? 0));
120+
$file->setCreatedAt($row['created_at'] ?? '');
121+
$files[] = $file;
122+
} catch (\Exception $e) {
123+
continue;
124+
}
125+
}
126+
127+
$result->closeCursor();
128+
129+
return $files;
130+
}
131+
132+
/**
133+
* Format a File entity as a SearchResultEntry
134+
*
135+
* @param File $file The file entity to format
136+
* @param IUser $user Current user
137+
* @return SearchResultEntry Formatted search result entry
138+
*/
139+
private function formatResult(File $file, IUser $user): SearchResultEntry {
140+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
141+
$thumbnailUrl = '';
142+
$subline = '';
143+
$icon = '';
144+
$path = '';
145+
146+
try {
147+
$nodes = $userFolder->getById($file->getNodeId());
148+
if (!empty($nodes)) {
149+
$node = array_shift($nodes);
150+
151+
$icon = $node->getMimetype() === FileInfo::MIMETYPE_FOLDER
152+
? 'icon-folder'
153+
: $this->mimeTypeDetector->mimeTypeIcon($node->getMimetype());
154+
155+
$thumbnailUrl = $this->urlGenerator->linkToRouteAbsolute(
156+
'core.Preview.getPreviewByFileId',
157+
[
158+
'x' => 32,
159+
'y' => 32,
160+
'fileId' => $node->getId()
161+
]
162+
);
163+
164+
$path = $userFolder->getRelativePath($node->getPath());
165+
$subline = $this->formatSubline($path);
166+
}
167+
} catch (\Exception $e) {
168+
}
169+
170+
$status = $this->getStatusLabel($file->getStatus());
171+
if ($status) {
172+
$subline .= $subline ? '' . $status : $status;
173+
}
174+
175+
$link = $this->urlGenerator->linkToRoute(
176+
'files.View.showFile',
177+
['fileid' => $file->getNodeId()]
178+
);
179+
180+
$searchResultEntry = new SearchResultEntry(
181+
$thumbnailUrl,
182+
$file->getName() ?? $this->l10n->t('Unnamed document'),
183+
$subline,
184+
$this->urlGenerator->getAbsoluteURL($link),
185+
$icon,
186+
);
187+
188+
$searchResultEntry->addAttribute('fileId', (string)$file->getNodeId());
189+
$searchResultEntry->addAttribute('path', $path);
190+
191+
return $searchResultEntry;
192+
}
193+
194+
/**
195+
* Format the subline showing file location
196+
*
197+
* @param string $path File path
198+
* @return string Formatted subline text
199+
*/
200+
private function formatSubline(string $path): string {
201+
if (strrpos($path, '/') > 0) {
202+
$path = ltrim(dirname($path), '/');
203+
return $this->l10n->t('in %s', [$path]);
204+
} else {
205+
return '';
206+
}
207+
}
208+
209+
/**
210+
* Get the translated label for a file status
211+
*
212+
* @param int|null $status File status code
213+
* @return string Translated status label
214+
*/
215+
private function getStatusLabel(?int $status): string {
216+
return match ($status) {
217+
File::STATUS_DRAFT => $this->l10n->t('Draft'),
218+
File::STATUS_ABLE_TO_SIGN => $this->l10n->t('Able to sign'),
219+
File::STATUS_PARTIAL_SIGNED => $this->l10n->t('Partially signed'),
220+
File::STATUS_SIGNED => $this->l10n->t('Signed'),
221+
File::STATUS_DELETED => $this->l10n->t('Deleted'),
222+
default => '',
223+
};
224+
}
225+
}

0 commit comments

Comments
 (0)