From 42542c8ac2c61df9178fb1aa25499091448ebf58 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 21 Sep 2023 21:22:13 +0200 Subject: [PATCH 01/10] enh(dashboard): add recent pages dashboard widget Signed-off-by: Arthur Schiwon --- lib/AppInfo/Application.php | 6 + lib/Dashboard/RecentPagesWidget.php | 165 ++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 lib/Dashboard/RecentPagesWidget.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 0b844d08d..c77f4ecb9 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -7,6 +7,7 @@ use Closure; use OCA\Circles\Events\CircleDestroyedEvent; use OCA\Collectives\CacheListener; +use OCA\Collectives\Dashboard\RecentPagesWidget; use OCA\Collectives\Db\CollectiveMapper; use OCA\Collectives\Db\PageMapper; use OCA\Collectives\Fs\UserFolderHelper; @@ -34,6 +35,7 @@ use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Collaboration\Reference\RenderReferenceEvent; +use OCP\Dashboard\IAPIWidgetV2; use OCP\Files\Config\IMountProviderCollection; use OCP\Files\IMimeTypeLoader; use OCP\IConfig; @@ -111,6 +113,10 @@ public function register(IRegistrationContext $context): void { $cacheListener = $this->getContainer()->get(CacheListener::class); $cacheListener->listen(); + + if (\interface_exists(IAPIWidgetV2::class)) { + $context->registerDashboardWidget(RecentPagesWidget::class); + } } public function boot(IBootcontext $context): void { diff --git a/lib/Dashboard/RecentPagesWidget.php b/lib/Dashboard/RecentPagesWidget.php new file mode 100644 index 000000000..02fe0d62a --- /dev/null +++ b/lib/Dashboard/RecentPagesWidget.php @@ -0,0 +1,165 @@ +userSession->getUser())) { + return new WidgetItems(); + } + + $collectives = $this->collectiveService->getCollectives($user->getUID()); + $results = []; + foreach ($collectives as $collective) { + // fetch pages from the current collective + $id = $collective->getId(); + $pages = $this->pageService->findAll($id, $user->getUID()); + + // sort pages and slice to the maximal necessary amount + usort($pages, function (PageInfo $a, PageInfo $b): int { + return $b->getTimestamp() - $a->getTimestamp(); + }); + $pages = array_slice($pages, 0, self::MAX_ITEMS); + + // prepare result entries + foreach ($pages as $page) { + $results[] = [ + 'timestamp' => $page->getTimestamp(), + 'page' => $page, + 'collective' => $collective + ]; + } + + // again sort result and slice to the max amount + usort($results, function (array $a, array $b): int { + return $b['timestamp'] - $a['timestamp']; + }); + $results = array_slice($results, 0, self::MAX_ITEMS); + } + + $items = []; + foreach ($results as $result) { + /* @var array{timestamp: int, page: PageInfo, collective: CollectiveInfo} $result */ + + $pathParts = [$result['collective']->getName()]; + if ($result['page']->getFilePath() !== '') { + $pathParts = array_merge($pathParts, explode('/', $result['page']->getFilePath())); + } + if ($result['page']->getFileName() !== 'Readme.md') { + $pathParts[] = $result['page']->getTitle(); + } + + $iconData = $result['page']->getEmoji() + ? $this->getEmojiAvatar($result['page']->getEmoji()) + : $this->getEmojiAvatar('🗒'); + //: $this->getFallbackDataIcon(); + + $items[] = new WidgetItem( + $result['page']->getTitle(), + $result['collective']->getName(), + $this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]), + 'data:image/svg+xml;base64,' . base64_encode($iconData), + (string)$result['timestamp'] + ); + } + + return new WidgetItems($items, $this->l10n->t('Add a collective')); + } + + protected function getFallbackDataIcon(): string { + // currently unused. Was an attempt to use the text icon which is also the fallback + // in Collectives itself. Probably because it is not really square, it is being + // rendered too dominant, compared to regular emojis + return ' +'; + } + + public function getReloadInterval(): int { + return self::REFRESH_INTERVAL_IN_SECS; + } + + public function getId(): string { + return 'collectives-recent-pages'; + } + + public function getTitle(): string { + return $this->l10n->t('Recent pages'); + } + + public function getOrder(): int { + return 6; + } + + public function getIconClass(): string { + return 'icon-collectives'; + } + + public function getUrl(): ?string { + return $this->urlGenerator->linkToRoute('collectives.collective.index'); + } + + public function load(): void { + } + + /** + * shamelessly copied from @nickvergessen​'s work at Talk + * @see https://github.com/nextcloud/spreed/blob/1e5c84ac14fbd1840c970ee7759e7bbdfbcba1a2/lib/Service/AvatarService.php#L174-L192 + */ + private string $svgTemplate = ' + + + {letter} + '; + + /** + * shamelessly copied from @nickvergessen​'s work at Talk + * @see https://github.com/nextcloud/spreed/blob/1e5c84ac14fbd1840c970ee7759e7bbdfbcba1a2/lib/Service/AvatarService.php#L240-L264 + */ + protected function getEmojiAvatar(string $emoji, string $fillColor = '00000000'): string { + return str_replace([ + '{letter}', + '{fill}', + '{font}', + ], [ + $emoji, + $fillColor, + implode(',', [ + "'Segoe UI'", + 'Roboto', + 'Oxygen-Sans', + 'Cantarell', + 'Ubuntu', + "'Helvetica Neue'", + 'Arial', + 'sans-serif', + "'Noto Color Emoji'", + "'Apple Color Emoji'", + "'Segoe UI Emoji'", + "'Segoe UI Symbol'", + "'Noto Sans'", + ]), + ], $this->svgTemplate); + } +} From 4c3a79422d910e598ce356472e824657310a8e7c Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 29 Sep 2023 11:54:33 +0200 Subject: [PATCH 02/10] perf(dashboard): load items from DB instead of using file sys abstraction - adds a simplified RecentPage model to avoid incomplete PageInfo instances - add RecentPageService with a method to fetch them for a specified user - adapts and simplifies RecenPagesWidget Signed-off-by: Arthur Schiwon --- lib/Dashboard/RecentPagesWidget.php | 60 +++----------- lib/Model/RecentPage.php | 56 +++++++++++++ lib/Service/RecentPagesService.php | 118 ++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 51 deletions(-) create mode 100644 lib/Model/RecentPage.php create mode 100644 lib/Service/RecentPagesService.php diff --git a/lib/Dashboard/RecentPagesWidget.php b/lib/Dashboard/RecentPagesWidget.php index 02fe0d62a..d54dc401e 100644 --- a/lib/Dashboard/RecentPagesWidget.php +++ b/lib/Dashboard/RecentPagesWidget.php @@ -2,10 +2,9 @@ namespace OCA\Collectives\Dashboard; -use OCA\Collectives\Model\CollectiveInfo; -use OCA\Collectives\Model\PageInfo; use OCA\Collectives\Service\CollectiveService; use OCA\Collectives\Service\PageService; +use OCA\Collectives\Service\RecentPagesService; use OCP\Dashboard\IReloadableWidget; use OCP\Dashboard\Model\WidgetItem; use OCP\Dashboard\Model\WidgetItems; @@ -23,6 +22,7 @@ public function __construct( protected IUserSession $userSession, protected PageService $pageService, protected CollectiveService $collectiveService, + protected RecentPagesService $recentPagesService, ) {} public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems { @@ -30,58 +30,16 @@ public function getItemsV2(string $userId, ?string $since = null, int $limit = 7 return new WidgetItems(); } - $collectives = $this->collectiveService->getCollectives($user->getUID()); - $results = []; - foreach ($collectives as $collective) { - // fetch pages from the current collective - $id = $collective->getId(); - $pages = $this->pageService->findAll($id, $user->getUID()); - - // sort pages and slice to the maximal necessary amount - usort($pages, function (PageInfo $a, PageInfo $b): int { - return $b->getTimestamp() - $a->getTimestamp(); - }); - $pages = array_slice($pages, 0, self::MAX_ITEMS); - - // prepare result entries - foreach ($pages as $page) { - $results[] = [ - 'timestamp' => $page->getTimestamp(), - 'page' => $page, - 'collective' => $collective - ]; - } - - // again sort result and slice to the max amount - usort($results, function (array $a, array $b): int { - return $b['timestamp'] - $a['timestamp']; - }); - $results = array_slice($results, 0, self::MAX_ITEMS); - } + $recentPages = $this->recentPagesService->forUser($user, self::MAX_ITEMS); $items = []; - foreach ($results as $result) { - /* @var array{timestamp: int, page: PageInfo, collective: CollectiveInfo} $result */ - - $pathParts = [$result['collective']->getName()]; - if ($result['page']->getFilePath() !== '') { - $pathParts = array_merge($pathParts, explode('/', $result['page']->getFilePath())); - } - if ($result['page']->getFileName() !== 'Readme.md') { - $pathParts[] = $result['page']->getTitle(); - } - - $iconData = $result['page']->getEmoji() - ? $this->getEmojiAvatar($result['page']->getEmoji()) - : $this->getEmojiAvatar('🗒'); - //: $this->getFallbackDataIcon(); - + foreach ($recentPages as $recentPage) { $items[] = new WidgetItem( - $result['page']->getTitle(), - $result['collective']->getName(), - $this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]), - 'data:image/svg+xml;base64,' . base64_encode($iconData), - (string)$result['timestamp'] + $recentPage->getTitle(), + $recentPage->getCollectiveName(), + $recentPage->getPageUrl(), + 'data:image/svg+xml;base64,' . base64_encode($this->getEmojiAvatar($recentPage->getEmoji())), + (string)$recentPage->getTimestamp() ); } diff --git a/lib/Model/RecentPage.php b/lib/Model/RecentPage.php new file mode 100644 index 000000000..c2a28f4d5 --- /dev/null +++ b/lib/Model/RecentPage.php @@ -0,0 +1,56 @@ +collectiveName; + } + + public function setCollectiveName(string $collectiveName): self { + $this->collectiveName = $collectiveName; + return $this; + } + + public function getPageUrl(): string { + return $this->pageUrl; + } + + public function setPageUrl(string $pageUrl): self { + $this->pageUrl = $pageUrl; + return $this; + } + + public function getTitle(): string { + return $this->title; + } + + public function setTitle(string $title): self { + $this->title = $title; + return $this; + } + + public function getEmoji(): string { + return $this->emoji; + } + + public function setEmoji(string $emoji): self { + $this->emoji = $emoji; + return $this; + } + + public function getTimestamp(): int { + return $this->timestamp; + } + + public function setTimestamp(int $timestamp): self { + $this->timestamp = $timestamp; + return $this; + } +} diff --git a/lib/Service/RecentPagesService.php b/lib/Service/RecentPagesService.php new file mode 100644 index 000000000..f874a9395 --- /dev/null +++ b/lib/Service/RecentPagesService.php @@ -0,0 +1,118 @@ +collectiveService->getCollectives($user->getUID()); + } catch (NotFoundException|NotPermittedException) { + return []; + } + + $qb = $this->dbc->getQueryBuilder(); + $appData = $this->getAppDataFolderName(); + $mimeTypeMd = $this->mimeTypeLoader->getId('text/markdown'); + + $expressions = []; + foreach ($collectives as $collective) { + $value = sprintf($appData . '/collectives/%d/%%', $collective->getId()); + $expressions[] = $qb->expr()->like('f.path', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR)); + } + + $qb->select('p.*', 'f.mtime as timestamp', 'f.name as filename', 'f.path as path') + ->from('filecache', 'f') + ->leftJoin('f', 'collectives_pages', 'p', $qb->expr()->eq('f.fileid', 'p.file_id')) + ->where($qb->expr()->orX(...$expressions)) + ->andWhere($qb->expr()->eq('f.mimetype', $qb->createNamedParameter($mimeTypeMd, IQueryBuilder::PARAM_INT))) + ->orderBy('f.mtime', 'DESC') + ->setMaxResults($limit); + + $r = $qb->executeQuery(); + + $pages = []; + $collectives = []; + while ($row = $r->fetch()) { + $collectiveId = (int)explode('/', $row['path'], 4)[2]; + if (!isset($collectives[$collectiveId])) { + try { + // collectives are not cached, but always read from DB, so keep them + $collectives[$collectiveId] = $this->collectiveService->getCollectiveInfo($collectiveId, $user->getUID()); + } catch (MissingDependencyException|NotFoundException|NotPermittedException) { + // just skip + continue; + } + } + + // cut out $appDataDir/collectives/%d/ prefix from front, and filename at the rear + $splitPath = explode('/', $row['path'], 4); + $internalPath = dirname(array_pop($splitPath)); + unset($splitPath); + + // prepare link and title + $pathParts = [$collectives[$collectiveId]->getName()]; + if ($internalPath !== '' && $internalPath !== '.') { + $pathParts = array_merge($pathParts, explode('/', $internalPath)); + } + if ($row['filename'] !== 'Readme.md') { + $pathParts[] = basename($row['filename'], PageInfo::SUFFIX); + $title = basename($row['filename'], PageInfo::SUFFIX); + } else { + $title = basename($internalPath); + } + $url = $this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]); + + // build result model + // not returning a PageInfo instance because it would be either incomplete or too expensive to build completely + $recentPage = new RecentPage(); + $recentPage->setCollectiveName($this->collectiveService->getCollectiveNameWithEmoji($collectives[$collectiveId])); + $recentPage->setTitle($title); + $recentPage->setPageUrl($url); + $recentPage->setTimestamp($row['timestamp']); + if ($row['emoji']) { + $recentPage->setEmoji($row['emoji']); + } + + $pages[] = $recentPage; + } + $r->closeCursor(); + + return $pages; + } + + private function getAppDataFolderName(): string { + $instanceId = $this->config->getSystemValueString('instanceid', ''); + if ($instanceId === '') { + throw new \RuntimeException('no instance id!'); + } + + return 'appdata_' . $instanceId; + } + +} From 47bb4ec118b0b3941cde4154b9075aadf55ce810 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 4 Oct 2023 15:05:15 +0200 Subject: [PATCH 03/10] fix(performance): reuse already fetched CollectiveInfo - CollectiveHelper methods now return array with collective id as key - by catch: make use of fluid setter at RecentPage Signed-off-by: Arthur Schiwon --- lib/Service/CollectiveHelper.php | 16 ++++++++++++---- lib/Service/CollectiveService.php | 2 +- lib/Service/RecentPagesService.php | 18 ++++++------------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/Service/CollectiveHelper.php b/lib/Service/CollectiveHelper.php index f245459b5..f97d8cbee 100644 --- a/lib/Service/CollectiveHelper.php +++ b/lib/Service/CollectiveHelper.php @@ -34,10 +34,12 @@ public function __construct(CollectiveMapper $collectiveMapper, * @param bool $getLevel * @param bool $getUserSettings * - * @return CollectiveInfo[] + * @return array * @throws NotFoundException * @throws NotPermittedException * @throws MissingDependencyException + * + * The array key of the returned result is the collective id. */ public function getCollectivesForUser(string $userId, bool $getLevel = true, bool $getUserSettings = true): array { $collectiveInfos = []; @@ -46,6 +48,7 @@ public function getCollectivesForUser(string $userId, bool $getLevel = true, boo return $circle->getSingleId(); }, $circles); $circles = array_combine($cids, $circles); + /** @var Collective[] $collectives */ $collectives = $this->collectiveMapper->findByCircleIds($cids); foreach ($collectives as $c) { $cid = $c->getCircleId(); @@ -59,7 +62,8 @@ public function getCollectivesForUser(string $userId, bool $getLevel = true, boo $userPageOrder = ($settings ? $settings->getSetting('page_order') : null) ?? Collective::defaultPageOrder; $userShowRecentPages = ($settings ? $settings->getSetting('show_recent_pages') : null) ?? Collective::defaultShowRecentPages; } - $collectiveInfos[] = new CollectiveInfo($c, + $collectiveInfos[$c->getId()] = new CollectiveInfo( + $c, $circle->getSanitizedName(), $level, null, @@ -73,10 +77,12 @@ public function getCollectivesForUser(string $userId, bool $getLevel = true, boo /** * @param string $userId * - * @return CollectiveInfo[] + * @return array * @throws NotFoundException * @throws NotPermittedException * @throws MissingDependencyException + * + * The array key of the returned result is the collective id. */ public function getCollectivesTrashForUser(string $userId): array { $collectiveInfos = []; @@ -85,10 +91,12 @@ public function getCollectivesTrashForUser(string $userId): array { return $circle->getSingleId(); }, $circles); $circles = array_combine($cids, $circles); + /** @var Collective[] $collectives */ $collectives = $this->collectiveMapper->findTrashByCircleIdsAndUser($cids, $userId); foreach ($collectives as $c) { $cid = $c->getCircleId(); - $collectiveInfos[] = new CollectiveInfo($c, + $collectiveInfos[$c->getId()] = new CollectiveInfo( + $c, $circles[$cid]->getSanitizedName(), $this->circleHelper->getLevel($cid, $userId) ); diff --git a/lib/Service/CollectiveService.php b/lib/Service/CollectiveService.php index cfc31ee21..ec4abf47d 100644 --- a/lib/Service/CollectiveService.php +++ b/lib/Service/CollectiveService.php @@ -104,7 +104,7 @@ public function getCollectiveWithShare(int $id, string $userId): CollectiveInfo /** * @param string $userId * - * @return CollectiveInfo[] + * @return array * @throws NotFoundException * @throws NotPermittedException * @throws MissingDependencyException diff --git a/lib/Service/RecentPagesService.php b/lib/Service/RecentPagesService.php index f874a9395..45870766b 100644 --- a/lib/Service/RecentPagesService.php +++ b/lib/Service/RecentPagesService.php @@ -57,17 +57,10 @@ public function forUser(IUser $user, int $limit = 10): array { $r = $qb->executeQuery(); $pages = []; - $collectives = []; while ($row = $r->fetch()) { $collectiveId = (int)explode('/', $row['path'], 4)[2]; if (!isset($collectives[$collectiveId])) { - try { - // collectives are not cached, but always read from DB, so keep them - $collectives[$collectiveId] = $this->collectiveService->getCollectiveInfo($collectiveId, $user->getUID()); - } catch (MissingDependencyException|NotFoundException|NotPermittedException) { - // just skip - continue; - } + continue; } // cut out $appDataDir/collectives/%d/ prefix from front, and filename at the rear @@ -91,10 +84,11 @@ public function forUser(IUser $user, int $limit = 10): array { // build result model // not returning a PageInfo instance because it would be either incomplete or too expensive to build completely $recentPage = new RecentPage(); - $recentPage->setCollectiveName($this->collectiveService->getCollectiveNameWithEmoji($collectives[$collectiveId])); - $recentPage->setTitle($title); - $recentPage->setPageUrl($url); - $recentPage->setTimestamp($row['timestamp']); + $recentPage + ->setCollectiveName($this->collectiveService->getCollectiveNameWithEmoji($collectives[$collectiveId])) + ->setTitle($title) + ->setPageUrl($url) + ->setTimestamp($row['timestamp']); if ($row['emoji']) { $recentPage->setEmoji($row['emoji']); } From c010f27648b8081e6806cc209f29849cc66c9778 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 4 Oct 2023 20:39:44 +0200 Subject: [PATCH 04/10] fix(php): restore PHP 7.4 compat Signed-off-by: Arthur Schiwon --- lib/Dashboard/RecentPagesWidget.php | 28 +++++++++++++++++++++------- lib/Service/RecentPagesService.php | 26 +++++++++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/Dashboard/RecentPagesWidget.php b/lib/Dashboard/RecentPagesWidget.php index d54dc401e..aea26c4ab 100644 --- a/lib/Dashboard/RecentPagesWidget.php +++ b/lib/Dashboard/RecentPagesWidget.php @@ -16,14 +16,28 @@ class RecentPagesWidget implements IReloadableWidget { public const REFRESH_INTERVAL_IN_SECS = 33; public const MAX_ITEMS = 10; + protected IL10N $l10n; + protected IURLGenerator $urlGenerator; + protected IUserSession $userSession; + protected PageService $pageService; + protected CollectiveService $collectiveService; + protected RecentPagesService $recentPagesService; + public function __construct( - protected IL10N $l10n, - protected IURLGenerator $urlGenerator, - protected IUserSession $userSession, - protected PageService $pageService, - protected CollectiveService $collectiveService, - protected RecentPagesService $recentPagesService, - ) {} + IL10N $l10n, + IURLGenerator $urlGenerator, + IUserSession $userSession, + PageService $pageService, + CollectiveService $collectiveService, + RecentPagesService $recentPagesService + ) { + $this->recentPagesService = $recentPagesService; + $this->collectiveService = $collectiveService; + $this->pageService = $pageService; + $this->userSession = $userSession; + $this->urlGenerator = $urlGenerator; + $this->l10n = $l10n; + } public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems { if (!($user = $this->userSession->getUser())) { diff --git a/lib/Service/RecentPagesService.php b/lib/Service/RecentPagesService.php index 45870766b..b26e03188 100644 --- a/lib/Service/RecentPagesService.php +++ b/lib/Service/RecentPagesService.php @@ -16,13 +16,25 @@ class RecentPagesService { + protected CollectiveService $collectiveService; + protected IURLGenerator $urlGenerator; + protected IDBConnection $dbc; + protected IConfig $config; + protected IMimeTypeLoader $mimeTypeLoader; + public function __construct( - protected CollectiveService $collectiveService, - protected IDBConnection $dbc, - protected IConfig $config, - protected IMimeTypeLoader $mimeTypeLoader, - protected IURLGenerator $urlGenerator, - ) { } + CollectiveService $collectiveService, + IDBConnection $dbc, + IConfig $config, + IMimeTypeLoader $mimeTypeLoader, + IURLGenerator $urlGenerator + ) { + $this->mimeTypeLoader = $mimeTypeLoader; + $this->config = $config; + $this->dbc = $dbc; + $this->urlGenerator = $urlGenerator; + $this->collectiveService = $collectiveService; + } /** * @return RecentPage[] @@ -32,7 +44,7 @@ public function __construct( public function forUser(IUser $user, int $limit = 10): array { try { $collectives = $this->collectiveService->getCollectives($user->getUID()); - } catch (NotFoundException|NotPermittedException) { + } catch (NotFoundException|NotPermittedException $e) { return []; } From 02f6a521977da0dbce409359747a41f8ae868d78 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 4 Oct 2023 20:47:58 +0200 Subject: [PATCH 05/10] refactor(model): remove fallback avatar logic from model Signed-off-by: Arthur Schiwon --- lib/Dashboard/RecentPagesWidget.php | 2 +- lib/Model/RecentPage.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Dashboard/RecentPagesWidget.php b/lib/Dashboard/RecentPagesWidget.php index aea26c4ab..f0db8a6ff 100644 --- a/lib/Dashboard/RecentPagesWidget.php +++ b/lib/Dashboard/RecentPagesWidget.php @@ -52,7 +52,7 @@ public function getItemsV2(string $userId, ?string $since = null, int $limit = 7 $recentPage->getTitle(), $recentPage->getCollectiveName(), $recentPage->getPageUrl(), - 'data:image/svg+xml;base64,' . base64_encode($this->getEmojiAvatar($recentPage->getEmoji())), + 'data:image/svg+xml;base64,' . base64_encode($this->getEmojiAvatar($recentPage->getEmoji() ?: '🗒')), (string)$recentPage->getTimestamp() ); } diff --git a/lib/Model/RecentPage.php b/lib/Model/RecentPage.php index c2a28f4d5..4866aac51 100644 --- a/lib/Model/RecentPage.php +++ b/lib/Model/RecentPage.php @@ -6,7 +6,7 @@ class RecentPage { protected string $collectiveName = ''; protected string $pageUrl = ''; protected string $title = ''; - protected string $emoji = '🗒'; + protected string $emoji = ''; protected int $timestamp = 0; public function getCollectiveName(): string { From b98e03dc63a801f6981308d309eca2adc50a2247 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 4 Oct 2023 21:08:11 +0200 Subject: [PATCH 06/10] style(cleanup): remove dead code Signed-off-by: Arthur Schiwon --- lib/Dashboard/RecentPagesWidget.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/Dashboard/RecentPagesWidget.php b/lib/Dashboard/RecentPagesWidget.php index f0db8a6ff..f04f97e1e 100644 --- a/lib/Dashboard/RecentPagesWidget.php +++ b/lib/Dashboard/RecentPagesWidget.php @@ -2,8 +2,6 @@ namespace OCA\Collectives\Dashboard; -use OCA\Collectives\Service\CollectiveService; -use OCA\Collectives\Service\PageService; use OCA\Collectives\Service\RecentPagesService; use OCP\Dashboard\IReloadableWidget; use OCP\Dashboard\Model\WidgetItem; @@ -19,21 +17,15 @@ class RecentPagesWidget implements IReloadableWidget { protected IL10N $l10n; protected IURLGenerator $urlGenerator; protected IUserSession $userSession; - protected PageService $pageService; - protected CollectiveService $collectiveService; protected RecentPagesService $recentPagesService; public function __construct( IL10N $l10n, IURLGenerator $urlGenerator, IUserSession $userSession, - PageService $pageService, - CollectiveService $collectiveService, RecentPagesService $recentPagesService ) { $this->recentPagesService = $recentPagesService; - $this->collectiveService = $collectiveService; - $this->pageService = $pageService; $this->userSession = $userSession; $this->urlGenerator = $urlGenerator; $this->l10n = $l10n; From 8324725f7ecfea73824b6ea8590dc098f3f80a09 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 2 Nov 2023 12:27:49 +0100 Subject: [PATCH 07/10] test(psalm): silence false positives - psalm issue: https://github.com/vimeo/psalm/issues/8258 Signed-off-by: Arthur Schiwon --- tests/psalm-baseline.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 01f922f32..b761c04d7 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,9 +1,18 @@ - + + RecentPagesWidget SearchablePageReferenceProvider + + IAPIWidgetV2 + + + + + IReloadableWidget + From eb8dc30ebc7001374f64a6c9d28195d1901f957d Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 3 Nov 2023 11:08:49 +0100 Subject: [PATCH 08/10] fix(frontend): revert return format of getCollectivesForUser Signed-off-by: Arthur Schiwon --- lib/Service/CollectiveHelper.php | 13 ++++--------- lib/Service/CollectiveService.php | 2 +- lib/Service/RecentPagesService.php | 9 ++++++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/Service/CollectiveHelper.php b/lib/Service/CollectiveHelper.php index f97d8cbee..0d763da10 100644 --- a/lib/Service/CollectiveHelper.php +++ b/lib/Service/CollectiveHelper.php @@ -34,12 +34,10 @@ public function __construct(CollectiveMapper $collectiveMapper, * @param bool $getLevel * @param bool $getUserSettings * - * @return array + * @return CollectiveInfo[] * @throws NotFoundException * @throws NotPermittedException * @throws MissingDependencyException - * - * The array key of the returned result is the collective id. */ public function getCollectivesForUser(string $userId, bool $getLevel = true, bool $getUserSettings = true): array { $collectiveInfos = []; @@ -62,7 +60,7 @@ public function getCollectivesForUser(string $userId, bool $getLevel = true, boo $userPageOrder = ($settings ? $settings->getSetting('page_order') : null) ?? Collective::defaultPageOrder; $userShowRecentPages = ($settings ? $settings->getSetting('show_recent_pages') : null) ?? Collective::defaultShowRecentPages; } - $collectiveInfos[$c->getId()] = new CollectiveInfo( + $collectiveInfos[] = new CollectiveInfo( $c, $circle->getSanitizedName(), $level, @@ -77,12 +75,10 @@ public function getCollectivesForUser(string $userId, bool $getLevel = true, boo /** * @param string $userId * - * @return array + * @return CollectiveInfo[] * @throws NotFoundException * @throws NotPermittedException * @throws MissingDependencyException - * - * The array key of the returned result is the collective id. */ public function getCollectivesTrashForUser(string $userId): array { $collectiveInfos = []; @@ -91,11 +87,10 @@ public function getCollectivesTrashForUser(string $userId): array { return $circle->getSingleId(); }, $circles); $circles = array_combine($cids, $circles); - /** @var Collective[] $collectives */ $collectives = $this->collectiveMapper->findTrashByCircleIdsAndUser($cids, $userId); foreach ($collectives as $c) { $cid = $c->getCircleId(); - $collectiveInfos[$c->getId()] = new CollectiveInfo( + $collectiveInfos[] = new CollectiveInfo( $c, $circles[$cid]->getSanitizedName(), $this->circleHelper->getLevel($cid, $userId) diff --git a/lib/Service/CollectiveService.php b/lib/Service/CollectiveService.php index ec4abf47d..cfc31ee21 100644 --- a/lib/Service/CollectiveService.php +++ b/lib/Service/CollectiveService.php @@ -104,7 +104,7 @@ public function getCollectiveWithShare(int $id, string $userId): CollectiveInfo /** * @param string $userId * - * @return array + * @return CollectiveInfo[] * @throws NotFoundException * @throws NotPermittedException * @throws MissingDependencyException diff --git a/lib/Service/RecentPagesService.php b/lib/Service/RecentPagesService.php index b26e03188..a4935d088 100644 --- a/lib/Service/RecentPagesService.php +++ b/lib/Service/RecentPagesService.php @@ -53,10 +53,13 @@ public function forUser(IUser $user, int $limit = 10): array { $mimeTypeMd = $this->mimeTypeLoader->getId('text/markdown'); $expressions = []; + $collectivesMap = []; foreach ($collectives as $collective) { $value = sprintf($appData . '/collectives/%d/%%', $collective->getId()); $expressions[] = $qb->expr()->like('f.path', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR)); + $collectivesMap[$collective->getId()] = $collective; } + unset($collectives); $qb->select('p.*', 'f.mtime as timestamp', 'f.name as filename', 'f.path as path') ->from('filecache', 'f') @@ -71,7 +74,7 @@ public function forUser(IUser $user, int $limit = 10): array { $pages = []; while ($row = $r->fetch()) { $collectiveId = (int)explode('/', $row['path'], 4)[2]; - if (!isset($collectives[$collectiveId])) { + if (!isset($collectivesMap[$collectiveId])) { continue; } @@ -81,7 +84,7 @@ public function forUser(IUser $user, int $limit = 10): array { unset($splitPath); // prepare link and title - $pathParts = [$collectives[$collectiveId]->getName()]; + $pathParts = [$collectivesMap[$collectiveId]->getName()]; if ($internalPath !== '' && $internalPath !== '.') { $pathParts = array_merge($pathParts, explode('/', $internalPath)); } @@ -97,7 +100,7 @@ public function forUser(IUser $user, int $limit = 10): array { // not returning a PageInfo instance because it would be either incomplete or too expensive to build completely $recentPage = new RecentPage(); $recentPage - ->setCollectiveName($this->collectiveService->getCollectiveNameWithEmoji($collectives[$collectiveId])) + ->setCollectiveName($this->collectiveService->getCollectiveNameWithEmoji($collectivesMap[$collectiveId])) ->setTitle($title) ->setPageUrl($url) ->setTimestamp($row['timestamp']); From ac9098cb3780872a86e9d6e1332baeb3d22fbf92 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 7 Nov 2023 16:31:03 +0100 Subject: [PATCH 09/10] fix(RecentPagesService): Add title for landing page and add fileId to links Signed-off-by: Jonas --- lib/Service/RecentPagesService.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecentPagesService.php b/lib/Service/RecentPagesService.php index a4935d088..a60912df1 100644 --- a/lib/Service/RecentPagesService.php +++ b/lib/Service/RecentPagesService.php @@ -11,6 +11,7 @@ use OCP\Files\IMimeTypeLoader; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; @@ -21,19 +22,22 @@ class RecentPagesService { protected IDBConnection $dbc; protected IConfig $config; protected IMimeTypeLoader $mimeTypeLoader; + protected IL10N $l10n; public function __construct( CollectiveService $collectiveService, IDBConnection $dbc, IConfig $config, IMimeTypeLoader $mimeTypeLoader, - IURLGenerator $urlGenerator + IURLGenerator $urlGenerator, + IL10N $l10n ) { $this->mimeTypeLoader = $mimeTypeLoader; $this->config = $config; $this->dbc = $dbc; $this->urlGenerator = $urlGenerator; $this->collectiveService = $collectiveService; + $this->l10n = $l10n; } /** @@ -91,10 +95,14 @@ public function forUser(IUser $user, int $limit = 10): array { if ($row['filename'] !== 'Readme.md') { $pathParts[] = basename($row['filename'], PageInfo::SUFFIX); $title = basename($row['filename'], PageInfo::SUFFIX); + } elseif ($internalPath === '' || $internalPath === '.') { + $title = $this->l10n->t('Landing page'); } else { $title = basename($internalPath); } - $url = $this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]); + + $fileIdSuffix = '?fileId=' . $row['file_id']; + $url = $this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]) . $fileIdSuffix; // build result model // not returning a PageInfo instance because it would be either incomplete or too expensive to build completely From aa7febfe72572f02163d52e20d945d1f8ad848d1 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 7 Nov 2023 16:28:37 +0100 Subject: [PATCH 10/10] test(cypress): Add simple Cypress test for the dashboard widget Signed-off-by: Jonas --- cypress/e2e/dashboard-widget.spec.js | 49 ++++++++++++++++++++++++++++ cypress/support/commands.js | 14 ++++++++ 2 files changed, 63 insertions(+) create mode 100644 cypress/e2e/dashboard-widget.spec.js diff --git a/cypress/e2e/dashboard-widget.spec.js b/cypress/e2e/dashboard-widget.spec.js new file mode 100644 index 000000000..c9535e531 --- /dev/null +++ b/cypress/e2e/dashboard-widget.spec.js @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) 2023 Jonas + * + * @author Jonas + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +/** + * Tests for Collectives dashboard widget. + */ + +describe('Collectives dashboard widget', function() { + if (Cypress.env('ncVersion') !== 'stable25') { + describe('Open dashboard widget', function() { + before(function() { + cy.loginAs('bob') + cy.enableDashboardWidget('collectives-recent-pages') + cy.visit('apps/collectives') + cy.deleteAndSeedCollective('Dashboard Collective1') + cy.seedPage('Page 1', '', 'Readme.md') + }) + it('Lists pages in the dashboard widget', function() { + cy.visit('/apps/dashboard/') + cy.get('.panel--header') + .contains('Recent pages') + cy.get('.panel--content').as('panelContent') + cy.get('@panelContent') + .find('li').should('contain', 'Landing page') + cy.get('@panelContent') + .find('li').should('contain', 'Page 1') + }) + }) + } +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 79975e4ae..1e82e3791 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -99,6 +99,20 @@ Cypress.Commands.add('setAppEnabled', (appName, value = true) => { }) }) +/** + * Enable dashboard widget + */ +Cypress.Commands.add('enableDashboardWidget', (widgetName) => { + cy.request('/csrftoken').then(({ body }) => { + const requesttoken = body.token + const api = `${Cypress.env('baseUrl')}/index.php/apps/dashboard/layout` + return axios.post(api, + { layout: widgetName }, + { headers: { requesttoken } }, + ) + }) +}) + /** * First delete, then seed a collective (to start fresh) */