Skip to content

Commit 278e4ba

Browse files
committed
feat: unified search
Signed-off-by: Crisciany Souza <[email protected]>
1 parent fae2922 commit 278e4ba

File tree

2 files changed

+224
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)