Dashboard widget for NC20
This commit is contained in:
parent
00d2fcfef1
commit
937c08754b
|
@ -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}',
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@include icon-black-white('notes', 'notes', 1);
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Notes\AppInfo;
|
||||
|
||||
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
|
||||
class BeforeTemplateRenderedListener implements IEventListener {
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof BeforeTemplateRenderedEvent)) {
|
||||
return;
|
||||
}
|
||||
if (!$event->isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
\OCP\Util::addStyle('notes', 'global');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Notes\AppInfo;
|
||||
|
||||
use OCP\Dashboard\IWidget;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class DashboardWidget implements IWidget {
|
||||
|
||||
/** @var IURLGenerator */
|
||||
private $url;
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
|
||||
public function __construct(
|
||||
IURLGenerator $url,
|
||||
IL10N $l10n
|
||||
) {
|
||||
$this->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');
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<DashboardWidget :items="items"
|
||||
:loading="loading"
|
||||
:show-more-text="t('notes', 'notes')"
|
||||
:show-more-url="showMoreUrl"
|
||||
>
|
||||
<template v-slot:default="{ item }">
|
||||
<DashboardWidgetItem
|
||||
:target-url="getItemTargetUrl(item)"
|
||||
:main-text="item.title"
|
||||
:sub-text="subtext(item)"
|
||||
>
|
||||
<template v-slot:avatar>
|
||||
<div
|
||||
class="note-item"
|
||||
:class="{ 'note-item-favorite': item.favorite, 'note-item-no-favorites': !hasFavorites }"
|
||||
/>
|
||||
</template>
|
||||
</DashboardWidgetItem>
|
||||
</template>
|
||||
<template v-slot:empty-content>
|
||||
<EmptyContent icon="icon-notes">
|
||||
<template #desc>
|
||||
<p class="notes-empty-content-label">
|
||||
{{ t('notes', 'No notes yet') }}
|
||||
</p>
|
||||
<p>
|
||||
<a :href="createNoteUrl" class="button">{{ t('note', 'New note') }}</a>
|
||||
</p>
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</template>
|
||||
</DashboardWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DashboardWidget, DashboardWidgetItem } from '@nextcloud/vue-dashboard'
|
||||
import { EmptyContent } from '@nextcloud/vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getDashboardData, categoryLabel } from '../NotesService'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
|
||||
components: {
|
||||
DashboardWidget,
|
||||
DashboardWidgetItem,
|
||||
EmptyContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
items: [],
|
||||
hasMoreItems: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showMoreUrl() {
|
||||
return this.hasMoreItems ? generateUrl('/apps/notes') : null
|
||||
},
|
||||
|
||||
hasFavorites() {
|
||||
return this.items.length > 0 && this.items[0].favorite
|
||||
},
|
||||
|
||||
createNoteUrl() {
|
||||
return generateUrl('/apps/notes/new')
|
||||
},
|
||||
|
||||
getItemTargetUrl() {
|
||||
return (note) => {
|
||||
return generateUrl('/apps/notes/note/' + note.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadDashboardData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadDashboardData() {
|
||||
getDashboardData().then(data => {
|
||||
this.items = data.items
|
||||
this.hasMoreItems = data.hasMoreItems
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
subtext(item) {
|
||||
return item.excerpt ? item.excerpt : categoryLabel(item.category)
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.note-item-favorite {
|
||||
background: var(--icon-star-dark-fc0);
|
||||
}
|
||||
|
||||
.note-item {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
flex-shrink: 0;
|
||||
background-size: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.note-item-no-favorites {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notes-empty-content-label {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
|
@ -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)
|
||||
})
|
||||
})
|
12
webpack.js
12
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)
|
||||
|
|
Loading…
Reference in New Issue