Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enh(dashboard): add recent pages dashboard widget #898

Merged
merged 10 commits into from
Nov 7, 2023
49 changes: 49 additions & 0 deletions cypress/e2e/dashboard-widget.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @copyright Copyright (c) 2023 Jonas <jonas@freesources.org>
*
* @author Jonas <jonas@freesources.org>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

/**
* 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')
})
})
}
})
14 changes: 14 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down
6 changes: 6 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
129 changes: 129 additions & 0 deletions lib/Dashboard/RecentPagesWidget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace OCA\Collectives\Dashboard;

use OCA\Collectives\Service\RecentPagesService;
use OCP\Dashboard\IReloadableWidget;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetItems;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserSession;

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 RecentPagesService $recentPagesService;

public function __construct(
IL10N $l10n,
IURLGenerator $urlGenerator,
IUserSession $userSession,
RecentPagesService $recentPagesService
) {
$this->recentPagesService = $recentPagesService;
$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())) {
return new WidgetItems();
}

$recentPages = $this->recentPagesService->forUser($user, self::MAX_ITEMS);

$items = [];
foreach ($recentPages as $recentPage) {
$items[] = new WidgetItem(
$recentPage->getTitle(),
$recentPage->getCollectiveName(),
$recentPage->getPageUrl(),
'data:image/svg+xml;base64,' . base64_encode($this->getEmojiAvatar($recentPage->getEmoji() ?: '🗒')),
(string)$recentPage->getTimestamp()
);
}

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 '<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg fill="#fffc" width="14" height="14" viewBox="0 0 18 25.5" xmlns="http://www.w3.org/2000/svg"><path d="M15.8,0H2.3C1,0,0,1,0,2.3v21c0,1.2,1,2.3,2.3,2.3h13.5c1.2,0,2.3-1,2.3-2.3v-21C18,1,17,0,15.8,0z M10.1,20.6H3.4v-2.2h6.8 V20.6z M14.6,16.1H3.4v-2.2h11.2V16.1z M14.6,11.6H3.4V9.4h11.2V11.6z M14.6,7.1H3.4V4.9h11.2V7.1z"></path></svg>';
}

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 = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="512" height="512" version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#{fill}"></rect>
<text x="50%" y="330" style="font-size:240px;font-family:{font};text-anchor:middle;">{letter}</text>
</svg>';

/**
* 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);
}
}
56 changes: 56 additions & 0 deletions lib/Model/RecentPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace OCA\Collectives\Model;

class RecentPage {
blizzz marked this conversation as resolved.
Show resolved Hide resolved
protected string $collectiveName = '';
protected string $pageUrl = '';
protected string $title = '';
protected string $emoji = '';
protected int $timestamp = 0;

public function getCollectiveName(): string {
return $this->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;
}
}
7 changes: 5 additions & 2 deletions lib/Service/CollectiveHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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();
Expand All @@ -59,7 +60,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[] = new CollectiveInfo(
$c,
$circle->getSanitizedName(),
$level,
null,
Expand Down Expand Up @@ -88,7 +90,8 @@ public function getCollectivesTrashForUser(string $userId): array {
$collectives = $this->collectiveMapper->findTrashByCircleIdsAndUser($cids, $userId);
foreach ($collectives as $c) {
$cid = $c->getCircleId();
$collectiveInfos[] = new CollectiveInfo($c,
$collectiveInfos[] = new CollectiveInfo(
$c,
$circles[$cid]->getSanitizedName(),
$this->circleHelper->getLevel($cid, $userId)
);
Expand Down
Loading
Loading