From 937c08754bdd984a0a8eb85b0bb9b0008f004586 Mon Sep 17 00:00:00 2001 From: korelstar Date: Sun, 20 Sep 2020 14:45:49 +0200 Subject: [PATCH] Dashboard widget for NC20 --- appinfo/routes.php | 11 ++ css/global.scss | 1 + lib/AppInfo/Application.php | 6 + .../BeforeTemplateRenderedListener.php | 21 +++ lib/AppInfo/DashboardWidget.php | 67 ++++++++++ lib/Controller/NotesController.php | 32 +++++ lib/Controller/PageController.php | 36 +++++- lib/Service/Note.php | 16 +++ lib/Service/NoteUtil.php | 9 ++ lib/Service/NotesService.php | 20 ++- package-lock.json | 26 ++++ package.json | 4 +- src/NotesService.js | 13 ++ src/components/Dashboard.vue | 121 ++++++++++++++++++ src/dashboard.js | 11 ++ webpack.js | 12 +- 16 files changed, 396 insertions(+), 10 deletions(-) create mode 100644 css/global.scss create mode 100644 lib/AppInfo/BeforeTemplateRenderedListener.php create mode 100644 lib/AppInfo/DashboardWidget.php create mode 100644 src/components/Dashboard.vue create mode 100644 src/dashboard.js diff --git a/appinfo/routes.php b/appinfo/routes.php index a51ee6d6..b399c381 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -13,6 +13,12 @@ return ['routes' => [ 'verb' => 'GET', 'postfix' => 'welcome', ], + [ + 'name' => 'page#create', + 'url' => '/new', + 'verb' => 'GET', + 'postfix' => 'new', + ], [ 'name' => 'page#index', 'url' => '/note/{id}', @@ -28,6 +34,11 @@ return ['routes' => [ 'url' => '/notes', 'verb' => 'GET', ], + [ + 'name' => 'notes#dashboard', + 'url' => '/notes/dashboard', + 'verb' => 'GET', + ], [ 'name' => 'notes#get', 'url' => '/notes/{id}', diff --git a/css/global.scss b/css/global.scss new file mode 100644 index 00000000..41ae33b2 --- /dev/null +++ b/css/global.scss @@ -0,0 +1 @@ +@include icon-black-white('notes', 'notes', 1); diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 5a45d74d..fff09806 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -8,6 +8,7 @@ use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; class Application extends App implements IBootstrap { public const APP_ID = 'notes'; @@ -19,6 +20,11 @@ class Application extends App implements IBootstrap { public function register(IRegistrationContext $context): void { $context->registerCapability(Capabilities::class); + $context->registerDashboardWidget(DashboardWidget::class); + $context->registerEventListener( + BeforeTemplateRenderedEvent::class, + BeforeTemplateRenderedListener::class + ); } public function boot(IBootContext $context): void { diff --git a/lib/AppInfo/BeforeTemplateRenderedListener.php b/lib/AppInfo/BeforeTemplateRenderedListener.php new file mode 100644 index 00000000..d2a76622 --- /dev/null +++ b/lib/AppInfo/BeforeTemplateRenderedListener.php @@ -0,0 +1,21 @@ +isLoggedIn()) { + return; + } + \OCP\Util::addStyle('notes', 'global'); + } +} diff --git a/lib/AppInfo/DashboardWidget.php b/lib/AppInfo/DashboardWidget.php new file mode 100644 index 00000000..166ec19e --- /dev/null +++ b/lib/AppInfo/DashboardWidget.php @@ -0,0 +1,67 @@ +url = $url; + $this->l10n = $l10n; + } + + /** + * @inheritDoc + */ + public function getId(): string { + return 'notes'; + } + + /** + * @inheritDoc + */ + public function getTitle(): string { + return $this->l10n->t('Notes'); + } + + /** + * @inheritDoc + */ + public function getOrder(): int { + return 30; + } + + /** + * @inheritDoc + */ + public function getIconClass(): string { + return 'icon-notes'; + } + + /** + * @inheritDoc + */ + public function getUrl(): ?string { + return $this->url->linkToRouteAbsolute('notes.page.index'); + } + + /** + * @inheritDoc + */ + public function load(): void { + \OCP\Util::addScript('notes', 'notes-dashboard'); + } +} diff --git a/lib/Controller/NotesController.php b/lib/Controller/NotesController.php index 471fc74d..c7bb58bc 100644 --- a/lib/Controller/NotesController.php +++ b/lib/Controller/NotesController.php @@ -114,6 +114,38 @@ class NotesController extends Controller { } + /** + * @NoAdminRequired + */ + public function dashboard() : JSONResponse { + return $this->helper->handleErrorResponse(function () { + $maxItems = 7; + $userId = $this->helper->getUID(); + $notes = $this->notesService->getTopNotes($userId, $maxItems + 1); + $hasMoreNotes = count($notes) > $maxItems; + $notes = array_slice($notes, 0, $maxItems); + $items = array_map(function ($note) { + $excerpt = ''; + try { + $excerpt = $note->getExcerpt(); + } catch (\Throwable $e) { + } + return [ + 'id' => $note->getId(), + 'title' => $note->getTitle(), + 'category' => $note->getCategory(), + 'favorite' => $note->getFavorite(), + 'excerpt' => $excerpt, + ]; + }, $notes); + return [ + 'items' => $items, + 'hasMoreItems' => $hasMoreNotes, + ]; + }); + } + + /** * @NoAdminRequired */ diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index b0e1de54..55eaadfc 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -4,22 +4,41 @@ declare(strict_types=1); namespace OCA\Notes\Controller; +use OCA\Notes\Service\NotesService; + use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\RedirectResponse; use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUserSession; class PageController extends Controller { - public function __construct(string $AppName, IRequest $request) { + /** @NotesService */ + private $notesService; + /** @var IUserSession */ + private $userSession; + /** @IURLGenerator */ + private $urlGenerator; + + public function __construct( + string $AppName, + IRequest $request, + NotesService $notesService, + IUserSession $userSession, + IURLGenerator $urlGenerator + ) { parent::__construct($AppName, $request); + $this->notesService = $notesService; + $this->userSession = $userSession; + $this->urlGenerator = $urlGenerator; } /** * @NoAdminRequired * @NoCSRFRequired - * - * @return TemplateResponse */ public function index() : TemplateResponse { $devMode = !is_file(dirname(__FILE__).'/../../js/notes-main.js'); @@ -35,4 +54,15 @@ class PageController extends Controller { return $response; } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function create() : RedirectResponse { + $note = $this->notesService->create($this->userSession->getUser()->getUID(), '', ''); + $note->setContent(''); + $url = $this->urlGenerator->linkToRoute('notes.page.index', [ 'id' => $note->getId() ]); + return new RedirectResponse($url); + } } diff --git a/lib/Service/Note.php b/lib/Service/Note.php index 205babf8..a44f2262 100644 --- a/lib/Service/Note.php +++ b/lib/Service/Note.php @@ -54,6 +54,22 @@ class Note { return $content; } + public function getExcerpt(int $maxlen = 100) : string { + $excerpt = trim($this->noteUtil->stripMarkdown($this->getContent())); + $title = $this->getTitle(); + if (!empty($title)) { + $length = strlen($title); + if (strncasecmp($excerpt, $title, $length) === 0) { + $excerpt = substr($excerpt, $length); + } + } + $excerpt = trim($excerpt); + if (strlen($excerpt) > $maxlen) { + $excerpt = substr($excerpt, 0, $maxlen) . '…'; + } + return str_replace("\n", "\u{2003}", $excerpt); + } + public function getModified() : int { return $this->file->getMTime(); } diff --git a/lib/Service/NoteUtil.php b/lib/Service/NoteUtil.php index fd336c77..d7286fb7 100644 --- a/lib/Service/NoteUtil.php +++ b/lib/Service/NoteUtil.php @@ -127,6 +127,15 @@ class NoteUtil { return trim($str); } + public function stripMarkdown(string $str) : string { + // prepare content: remove markdown characters and empty spaces + $str = preg_replace("/^\s*[*+-]\s+/mu", "", $str); // list item + $str = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $str); // headline + $str = preg_replace("/^(=+|-+)$/mu", "", $str); // separate line for headline + $str = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $str); // emphasis + return $str; + } + /** * Finds a folder and creates it if non-existent * @param string $path path to the folder diff --git a/lib/Service/NotesService.php b/lib/Service/NotesService.php index 00d199cf..3c940b8e 100644 --- a/lib/Service/NotesService.php +++ b/lib/Service/NotesService.php @@ -37,6 +37,20 @@ class NotesService { return [ 'notes' => $notes, 'categories' => $data['categories'] ]; } + public function getTopNotes(string $userId, int $count) : array { + $notes = $this->getAll($userId)['notes']; + usort($notes, function (Note $a, Note $b) { + $favA = $a->getFavorite(); + $favB = $b->getFavorite(); + if ($favA === $favB) { + return $b->getModified() - $a->getModified(); + } else { + return $favA > $favB ? -1 : 1; + } + }); + return array_slice($notes, 0, $count); + } + public function get(string $userId, int $id) : Note { $notesFolder = $this->getNotesFolder($userId); $note = new Note($this->getFileById($notesFolder, $id), $notesFolder, $this->noteUtil); @@ -89,11 +103,7 @@ class NotesService { } public function getTitleFromContent(string $content) : string { - // prepare content: remove markdown characters and empty spaces - $content = preg_replace("/^\s*[*+-]\s+/mu", "", $content); // list item - $content = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $content); // headline - $content = preg_replace("/^(=+|-+)$/mu", "", $content); // separate line for headline - $content = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $content); // emphasis + $content = $this->noteUtil->stripMarkdown($content); return $this->noteUtil->getSafeTitle($content); } diff --git a/package-lock.json b/package-lock.json index 117db543..a6a1f08c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2731,6 +2731,16 @@ } } }, + "@nextcloud/vue-dashboard": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@nextcloud/vue-dashboard/-/vue-dashboard-1.0.1.tgz", + "integrity": "sha512-QKOf0qm4UYobuC3djLYwICSuj29H6xnm24IHetc0AMsrfhwPNN1/CC5IWEl1uKCCvwI0NA02HXCVFLtUErlgyg==", + "requires": { + "@nextcloud/vue": "^2.3.0", + "core-js": "^3.6.4", + "vue": "^2.6.11" + } + }, "@nextcloud/webpack-vue-config": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@nextcloud/webpack-vue-config/-/webpack-vue-config-1.4.1.tgz", @@ -11328,6 +11338,16 @@ } } }, + "webpack-merge": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.1.4.tgz", + "integrity": "sha512-LSmRD59mxREGkCBm9PCW3AaV4doDqxykGlx1NvioEE0FgkT2GQI54Wyvg39ptkiq2T11eRVoV39udNPsQvK+QQ==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, "webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", @@ -11370,6 +11390,12 @@ "string-width": "^1.0.2 || 2" } }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index f75a5796..0ae89929 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@nextcloud/moment": "^1.1.1", "@nextcloud/router": "^1.2.0", "@nextcloud/vue": "^2.6.6", + "@nextcloud/vue-dashboard": "^1.0.1", "@nextcloud/webpack-vue-config": "^1.4.1", "easymde": "^2.12.0", "markdown-it": "^11.0.1", @@ -65,6 +66,7 @@ "vue-loader": "^15.9.3", "vue-template-compiler": "^2.6.12", "webpack": "^4.44.2", - "webpack-cli": "^3.3.12" + "webpack-cli": "^3.3.12", + "webpack-merge": "^5.1.4" } } diff --git a/src/NotesService.js b/src/NotesService.js index b0e614c7..f6868e67 100644 --- a/src/NotesService.js +++ b/src/NotesService.js @@ -45,6 +45,19 @@ export const setSettings = settings => { }) } +export const getDashboardData = () => { + return axios + .get(url('/notes/dashboard')) + .then(response => { + return response.data + }) + .catch(err => { + console.error(err) + handleSyncError(t('notes', 'Fetching notes for dashboard has failed.'), err) + throw err + }) +} + export const fetchNotes = () => { const lastETag = store.state.sync.etag const lastModified = store.state.sync.lastModified diff --git a/src/components/Dashboard.vue b/src/components/Dashboard.vue new file mode 100644 index 00000000..183f023e --- /dev/null +++ b/src/components/Dashboard.vue @@ -0,0 +1,121 @@ + + + + diff --git a/src/dashboard.js b/src/dashboard.js new file mode 100644 index 00000000..4198da7e --- /dev/null +++ b/src/dashboard.js @@ -0,0 +1,11 @@ +import Vue from 'vue' +import Dashboard from './components/Dashboard' + +Vue.mixin({ methods: { t, n } }) + +document.addEventListener('DOMContentLoaded', () => { + OCA.Dashboard.register('notes', (el) => { + const View = Vue.extend(Dashboard) + new View().$mount(el) + }) +}) diff --git a/webpack.js b/webpack.js index 719f4065..6ddbea92 100644 --- a/webpack.js +++ b/webpack.js @@ -1 +1,11 @@ -module.exports = require('@nextcloud/webpack-vue-config') +const webpackConfig = require('@nextcloud/webpack-vue-config') +const path = require('path') +const { merge } = require('webpack-merge') + +const config = { + entry: { + dashboard: path.join(__dirname, 'src', 'dashboard.js'), + }, +} + +module.exports = merge(webpackConfig, config)