Skip to content

Commit 79a6f7a

Browse files
committed
feat(search): add profile field search provider
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 1a133a4 commit 79a6f7a

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\ProfileFields\Search;
11+
12+
use OCP\IL10N;
13+
use OCP\IURLGenerator;
14+
use OCP\IUser;
15+
use OCP\Search\IProvider;
16+
use OCP\Search\ISearchQuery;
17+
use OCP\Search\SearchResult;
18+
use OCP\Search\SearchResultEntry;
19+
20+
class ProfileFieldSearchProvider implements IProvider {
21+
private const MIN_SEARCH_LENGTH = 2;
22+
23+
public function __construct(
24+
private IL10N $l10n,
25+
private IURLGenerator $urlGenerator,
26+
private ProfileFieldDirectorySearchService $searchService,
27+
) {
28+
}
29+
30+
#[\Override]
31+
public function getId(): string {
32+
return 'profile_fields.directory';
33+
}
34+
35+
#[\Override]
36+
public function getName(): string {
37+
return $this->l10n->t('Profile directory');
38+
}
39+
40+
#[\Override]
41+
public function getOrder(string $route, array $routeParameters): ?int {
42+
return str_starts_with($route, 'settings.Users.usersList') ? 35 : 65;
43+
}
44+
45+
#[\Override]
46+
public function search(IUser $user, ISearchQuery $query): SearchResult {
47+
$term = trim($query->getTerm());
48+
if (mb_strlen($term) < self::MIN_SEARCH_LENGTH) {
49+
return SearchResult::complete($this->getName(), []);
50+
}
51+
52+
$cursor = $this->normalizeCursor($query->getCursor());
53+
$result = $this->searchService->search($user, $term, $query->getLimit(), $cursor);
54+
$entries = array_map(fn (array $item): SearchResultEntry => $this->buildEntry($item), $result['items']);
55+
if ($cursor + count($entries) >= $result['total']) {
56+
return SearchResult::complete($this->getName(), $entries);
57+
}
58+
59+
return SearchResult::paginated(
60+
$this->getName(),
61+
$entries,
62+
$cursor + count($entries),
63+
);
64+
}
65+
66+
private function normalizeCursor(int|string|null $cursor): int {
67+
if ($cursor === null || $cursor === '') {
68+
return 0;
69+
}
70+
71+
if (is_int($cursor)) {
72+
return $cursor;
73+
}
74+
75+
if (preg_match('/^-?\d+$/', $cursor) === 1) {
76+
return (int)$cursor;
77+
}
78+
79+
return 0;
80+
}
81+
82+
/**
83+
* @param array{
84+
* user_uid: string,
85+
* display_name: string,
86+
* matched_fields: list<array{
87+
* field_key: string,
88+
* field_label: string,
89+
* value: string
90+
* }>
91+
* } $item
92+
*/
93+
private function buildEntry(array $item): SearchResultEntry {
94+
$thumbnailUrl = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', [
95+
'userId' => $item['user_uid'],
96+
'size' => 64,
97+
]);
98+
$resourceUrl = $this->urlGenerator->linkToRouteAbsolute('settings.Users.usersList') . '?search=' . rawurlencode($item['user_uid']);
99+
100+
return new SearchResultEntry(
101+
$thumbnailUrl,
102+
$item['display_name'],
103+
$this->buildSubline($item['matched_fields']),
104+
$resourceUrl,
105+
'icon-user',
106+
true,
107+
);
108+
}
109+
110+
/**
111+
* @param list<array{field_key: string, field_label: string, value: string}> $matchedFields
112+
*/
113+
private function buildSubline(array $matchedFields): string {
114+
$parts = array_map(
115+
static fn (array $match): string => sprintf('%s: %s', $match['field_label'], $match['value']),
116+
$matchedFields,
117+
);
118+
119+
return implode('', $parts);
120+
}
121+
}

0 commit comments

Comments
 (0)