From 29a0db74336fa2d72ea7880d41935d4590835c7d Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Sun, 27 Oct 2024 17:38:05 +0100 Subject: [PATCH] refs #304 add unified search provider for projects Signed-off-by: Julien Veyssier --- lib/AppInfo/Application.php | 12 +- .../BeforeTemplateRenderedListener.php | 42 +++++ ...ider.php => CospendSearchBillProvider.php} | 25 +-- lib/Search/CospendSearchProjectProvider.php | 160 ++++++++++++++++++ lib/Service/LocalProjectService.php | 3 + 5 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 lib/Listener/BeforeTemplateRenderedListener.php rename lib/Search/{CospendSearchProvider.php => CospendSearchBillProvider.php} (92%) create mode 100644 lib/Search/CospendSearchProjectProvider.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d44ed9a0..4af839d9 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -14,18 +14,21 @@ namespace OCA\Cospend\AppInfo; use OCA\Cospend\Capabilities; use OCA\Cospend\Dashboard\CospendWidget; use OCA\Cospend\Federation\CloudFederationProviderCospend; +use OCA\Cospend\Listener\BeforeTemplateRenderedListener; use OCA\Cospend\Listener\CSPListener; use OCA\Cospend\Middleware\FederationMiddleware; use OCA\Cospend\Middleware\PublicAuthMiddleware; use OCA\Cospend\Middleware\UserPermissionMiddleware; use OCA\Cospend\Notification\Notifier; -use OCA\Cospend\Search\CospendSearchProvider; +use OCA\Cospend\Search\CospendSearchBillProvider; +use OCA\Cospend\Search\CospendSearchProjectProvider; use OCA\Cospend\UserMigration\UserMigrator; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\Federation\ICloudFederationProvider; use OCP\Federation\ICloudFederationProviderManager; use OCP\IConfig; @@ -102,14 +105,13 @@ class Application extends App implements IBootstrap { public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); // TODO - // - rename db columns with underscores, change new APIs param names, keep a second jsonSerialize method for old APIs - // - check if it makes sense to have a paypal integration // - check how to switch to numerical project IDs (keep unique slug for client compatibility) } public function register(IRegistrationContext $context): void { $context->registerNotifierService(Notifier::class); - $context->registerSearchProvider(CospendSearchProvider::class); + $context->registerSearchProvider(CospendSearchBillProvider::class); + $context->registerSearchProvider(CospendSearchProjectProvider::class); $context->registerDashboardWidget(CospendWidget::class); $context->registerUserMigrator(UserMigrator::class); @@ -120,10 +122,10 @@ class Application extends App implements IBootstrap { $context->registerCapability(Capabilities::class); $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); } public function boot(IBootContext $context): void { - Util::addStyle(self::APP_ID, 'cospend-search'); $context->injectFn([$this, 'registerCloudFederationProviderManager']); } diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php new file mode 100644 index 00000000..3058cc70 --- /dev/null +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -0,0 +1,42 @@ + + */ +class BeforeTemplateRenderedListener implements IEventListener { + + public function __construct( + private IUserSession $userSession, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeTemplateRenderedEvent)) { + // Unrelated + return; + } + + if ($event->getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_USER) { + return; + } + + if (!$this->userSession->getUser() instanceof IUser) { + return; + } + + Util::addStyle(Application::APP_ID, 'cospend-search'); + } +} diff --git a/lib/Search/CospendSearchProvider.php b/lib/Search/CospendSearchBillProvider.php similarity index 92% rename from lib/Search/CospendSearchProvider.php rename to lib/Search/CospendSearchBillProvider.php index 0dd81994..f0142df6 100644 --- a/lib/Search/CospendSearchProvider.php +++ b/lib/Search/CospendSearchBillProvider.php @@ -39,7 +39,7 @@ use OCP\Search\ISearchQuery; use OCP\Search\SearchResult; use OCP\Search\SearchResultEntry; -class CospendSearchProvider implements IProvider { +class CospendSearchBillProvider implements IProvider { public function __construct( private IAppManager $appManager, @@ -55,21 +55,21 @@ class CospendSearchProvider implements IProvider { * @inheritDoc */ public function getId(): string { - return 'cospend-search'; + return 'cospend-search-bills'; } /** * @inheritDoc */ public function getName(): string { - return $this->l10n->t('Cospend'); + return $this->l10n->t('Cospend bills'); } /** * @inheritDoc */ public function getOrder(string $route, array $routeParameters): int { - if (strpos($route, Application::APP_ID . '.') === 0) { + if (str_starts_with($route, Application::APP_ID . '.')) { // Active app, prefer Cospend results return -1; } @@ -81,7 +81,7 @@ class CospendSearchProvider implements IProvider { * @inheritDoc */ public function search(IUser $user, ISearchQuery $query): SearchResult { - if (!$this->appManager->isEnabledForUser('cospend', $user)) { + if (!$this->appManager->isEnabledForUser(Application::APP_ID, $user)) { return SearchResult::complete($this->getName(), []); } @@ -181,17 +181,20 @@ class CospendSearchProvider implements IProvider { /** * @param string $projectId + * @param int $billId * @return string */ protected function getDeepLinkToCospendApp(string $projectId, int $billId): string { - return $this->urlGenerator->getAbsoluteURL( - $this->urlGenerator->linkToRoute('cospend.page.indexBill', [ - 'projectId' => $projectId, - 'billId' => $billId, - ]) - ); + return $this->urlGenerator->linkToRouteAbsolute('cospend.page.indexBill', [ + 'projectId' => $projectId, + 'billId' => $billId, + ]); } + /** + * @param array $bill + * @return string + */ protected function getThumbnailUrl(array $bill): string { if ($bill['payer_user_id']) { return $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $bill['payer_user_id'], 'size' => 44]); diff --git a/lib/Search/CospendSearchProjectProvider.php b/lib/Search/CospendSearchProjectProvider.php new file mode 100644 index 00000000..7ca52845 --- /dev/null +++ b/lib/Search/CospendSearchProjectProvider.php @@ -0,0 +1,160 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\Cospend\Search; + +use OCA\Cospend\AppInfo\Application; +use OCA\Cospend\Service\LocalProjectService; +use OCP\App\IAppManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Search\IProvider; + +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; + +class CospendSearchProjectProvider implements IProvider { + + public function __construct( + private IAppManager $appManager, + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private LocalProjectService $projectService, + private IUserManager $userManager, + ) { + } + + /** + * @inheritDoc + */ + public function getId(): string { + return 'cospend-search-projects'; + } + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l10n->t('Cospend projects'); + } + + /** + * @inheritDoc + */ + public function getOrder(string $route, array $routeParameters): int { + if (str_starts_with($route, Application::APP_ID . '.')) { + // Active app, prefer Cospend results + return -1; + } + + return 20; + } + + /** + * @inheritDoc + */ + public function search(IUser $user, ISearchQuery $query): SearchResult { + if (!$this->appManager->isEnabledForUser(Application::APP_ID, $user)) { + return SearchResult::complete($this->getName(), []); + } + + $limit = $query->getLimit(); + $term = $query->getTerm(); + $offset = $query->getCursor(); + $offset = $offset ? (int)$offset : 0; + + $resultBills = []; + + // get user's projects + $projects = $this->projectService->getLocalProjects($user->getUID()); + $resultProjects = array_filter($projects, static function(array $project) use ($term) { + $projectName = $project['name']; + return str_contains(strtolower($projectName), strtolower($term)); + }); + + $resultProjects = array_slice($resultProjects, $offset, $limit); + + // build formatted + $formattedResults = array_map(function (array $project): SearchResultEntry { + $thumbnailUrl = $this->getThumbnailUrl($project); + return new SearchResultEntry( + $thumbnailUrl, + $this->getMainText($project), + $this->getSubline($project), + $this->getDeepLinkToCospendApp($project['id']), + $thumbnailUrl === '' ? 'icon-cospend-search-fallback' : '', + true + ); + }, $resultProjects); + + return SearchResult::paginated( + $this->getName(), + $formattedResults, + $offset + $limit + ); + } + + /** + * @param array $project + * @return string + */ + protected function getMainText(array $project): string { + return $project['name']; + } + + /** + * @param array $project + * @return string + */ + protected function getSubline(array $project): string { + $ownerId = $project['userid']; + $owner = $this->userManager->get($ownerId); + if ($owner === null) { + return ''; + } + return $this->l10n->t('Owned by %1$s', [$owner->getDisplayName()]); + } + + /** + * @param string $projectId + * @return string + */ + protected function getDeepLinkToCospendApp(string $projectId): string { + return $this->urlGenerator->linkToRouteAbsolute('cospend.page.indexProject', [ + 'projectId' => $projectId, + ]); + } + + /** + * @param array $project + * @return string + */ + protected function getThumbnailUrl(array $project): string { + return ''; + // return $this->urlGenerator->imagePath(Application::APP_ID, ''); + } +} diff --git a/lib/Service/LocalProjectService.php b/lib/Service/LocalProjectService.php index 31fe0936..7ee7c7be 100644 --- a/lib/Service/LocalProjectService.php +++ b/lib/Service/LocalProjectService.php @@ -1759,6 +1759,9 @@ class LocalProjectService implements IProjectService { * * @param string $userId * @return array + * @throws CospendBasicException + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ public function getLocalProjects(string $userId): array {