Add dashboard widget to show the latest notes. Issue #51

p.s: I would like to be able to select a particular tag to show, but
none of the dashboard widgets are configurable.

I hate increasing the app size from 300k to almost 3 mb for something
so simple, but we must adapt to the majority and use vue here.
This commit is contained in:
Matias De lellis
2022-05-25 10:29:20 -03:00
parent e6d52bfa5a
commit b6db15d8d2
14 changed files with 21727 additions and 308 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
build build
js/templates.js js/templates.js
js/quicknotes-dashboard.js*
node_modules node_modules
vendor vendor
translationfiles/ translationfiles/

View File

@@ -26,8 +26,8 @@ package_name=$(app_name)
cert_dir=$(HOME)/.nextcloud/certificates cert_dir=$(HOME)/.nextcloud/certificates
# building the javascript # building the javascript
all: build all: build
build: deps
# L10N Rules # L10N Rules
@@ -77,9 +77,19 @@ depsmin:
cp node_modules/medium-editor-autolist/dist/autolist.min.js vendor/autolist.js cp node_modules/medium-editor-autolist/dist/autolist.min.js vendor/autolist.js
cp node_modules/lozad/dist/lozad.min.js vendor/lozad.js cp node_modules/lozad/dist/lozad.min.js vendor/lozad.js
# Build Rules
build-vue:
npm run build
js-templates: js-templates:
node_modules/handlebars/bin/handlebars js/templates -f js/templates.js node_modules/handlebars/bin/handlebars js/templates -f js/templates.js
build: depsmin js-templates build-vue
@echo ""
@echo "Build done. You can enable the application in Nextcloud."
# Clean
clean: clean:
rm -rf $(build_dir) rm -rf $(build_dir)
@@ -91,7 +101,7 @@ distclean: clean
dist: appstore dist: appstore
appstore: distclean depsmin appstore: distclean build
mkdir -p $(sign_dir) mkdir -p $(sign_dir)
rsync -a \ rsync -a \
--exclude='.*' \ --exclude='.*' \
@@ -107,8 +117,11 @@ appstore: distclean depsmin
--exclude=vendor/bin \ --exclude=vendor/bin \
--exclude=node_modules \ --exclude=node_modules \
--exclude=js/templates \ --exclude=js/templates \
--exclude=src \
--exclude=templates/fake.php \ --exclude=templates/fake.php \
--exclude=translation* \ --exclude=translation* \
--exclude=webpack*.js \
--exclude=psalm.xml \
$(project_dir) $(sign_dir) $(project_dir) $(sign_dir)
@echo "Signing…" @echo "Signing…"
php ../../occ integrity:sign-app \ php ../../occ integrity:sign-app \

View File

@@ -11,6 +11,12 @@ return ['resources' =>
'url' => '/', 'url' => '/',
'verb' => 'GET' 'verb' => 'GET'
], ],
// Dashboard
[
'name' => 'note#dashboard',
'url' => '/notes/dashboard',
'verb' => 'GET',
],
// Share // Share
[ [
'name' => 'share#destroy', 'name' => 'share#destroy',

7
css/global.scss Normal file
View File

@@ -0,0 +1,7 @@
.icon-quicknotes {
@include icon-color('app', 'quicknotes', $color-black);
}
.icon-pinned {
@include icon-color('pinned', 'quicknotes', $color-black);
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
/* /*
* @copyright 2016-2021 Matias De lellis <mati86dl@gmail.com> * @copyright 2016-2022 Matias De lellis <mati86dl@gmail.com>
* *
* @author 2016 Matias De lellis <mati86dl@gmail.com> * @author 2016 Matias De lellis <mati86dl@gmail.com>
* *
@@ -27,6 +27,8 @@ use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\IL10N; use OCP\IL10N;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\IServerContainer; use OCP\IServerContainer;
@@ -48,6 +50,11 @@ class Application extends App implements IBootstrap {
public function register(IRegistrationContext $context): void { public function register(IRegistrationContext $context): void {
$context->registerSearchProvider(NoteSearchProvider::class); $context->registerSearchProvider(NoteSearchProvider::class);
$context->registerCapability(Capabilities::class); $context->registerCapability(Capabilities::class);
$context->registerDashboardWidget(DashboardWidget::class);
$context->registerEventListener(
BeforeTemplateRenderedEvent::class,
BeforeTemplateRenderedListener::class
);
} }
public function boot(IBootContext $context): void { public function boot(IBootContext $context): void {

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace OCA\QuickNotes\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('quicknotes', 'global');
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace OCA\QuickNotes\AppInfo;
use OCP\Dashboard\IWidget;
use OCP\IL10N;
use OCP\IURLGenerator;
class DashboardWidget implements IWidget {
private IURLGenerator $url;
private IL10N $l10n;
public function __construct(IURLGenerator $url,
IL10N $l10n)
{
$this->url = $url;
$this->l10n = $l10n;
}
/**
* @inheritDoc
*/
public function getId(): string {
return 'quicknotes';
}
/**
* @inheritDoc
*/
public function getTitle(): string {
return $this->l10n->t('Quick notes');
}
/**
* @inheritDoc
*/
public function getOrder(): int {
return 30;
}
/**
* @inheritDoc
*/
public function getIconClass(): string {
return 'icon-quicknotes';
}
/**
* @inheritDoc
*/
public function getUrl(): ?string {
return $this->url->linkToRouteAbsolute('quicknotes.page.index');
}
/**
* @inheritDoc
*/
public function load(): void {
\OCP\Util::addScript('quicknotes', 'quicknotes-dashboard');
}
}

View File

@@ -67,6 +67,41 @@ class NoteController extends Controller {
return $response; return $response;
} }
/**
* @NoAdminRequired
*/
public function dashboard(): JSONResponse {
$notes = $this->noteService->getAll($this->userId);
if (count($notes) === 0) {
return new JSONResponse([
'notes' => []
]);
}
$items = array_map(function ($note) {
return [
'id' => $note->getId(),
'title' => strip_tags($note->getTitle()),
'content' => strip_tags($note->getContent()),
'pinned' => $note->getIsPinned(),
'timestamp' => $note->getTimestamp(),
];
}, $notes);
usort($items, function ($a, $b) {
if ($a['pinned'] == $b['pinned'])
return $b['timestamp'] - $a['timestamp'];
if ($a['pinned'] && !$b['pinned'])
return -1;
if (!$a['pinned'] && $b['pinned'])
return 1;
});
return new JSONResponse([
'notes' => array_slice($items, 0, 7)
]);
}
/** /**
* @NoAdminRequired * @NoAdminRequired
* *

21162
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "quicknotes", "name": "quicknotes",
"version": "0.1.9", "version": "0.8.1",
"description": "Quick notes with a basic rich text", "description": "Quick notes with a basic rich text",
"main": "script.js", "main": "script.js",
"directories": { "directories": {
@@ -8,7 +8,7 @@
"test": "tests" "test": "tests"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "build": "webpack --node-env production --progress"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -25,6 +25,19 @@
"isotope-layout": "^3.0.6", "isotope-layout": "^3.0.6",
"lozad": "^1.15.0", "lozad": "^1.15.0",
"medium-editor": "^5.23.3", "medium-editor": "^5.23.3",
"medium-editor-autolist": "^1.0.1" "medium-editor-autolist": "^1.0.1",
"@nextcloud/axios": "^1.9.0",
"@nextcloud/dialogs": "^3.1.2",
"@nextcloud/router": "^2.0.0",
"@nextcloud/vue": "^5.3.1",
"@nextcloud/vue-dashboard": "^2.0.1",
"vue": "^2.6.14"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.2.0",
"@nextcloud/eslint-config": "^7.0.2",
"@nextcloud/stylelint-config": "^2.1.2",
"@nextcloud/webpack-vue-config": "^5.1.0"
} }
} }

21
src/NotesService.js Normal file
View File

@@ -0,0 +1,21 @@
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
function url(url) {
url = `apps/quicknotes${url}`
return generateUrl(url)
}
export const getDashboardData = () => {
return axios
.get(url('/notes/dashboard'))
.then(response => {
return response.data
})
.catch(err => {
console.error(err)
showError(t('notes', 'Fetching notes for dashboard has failed.'))
throw err
})
}

View File

@@ -0,0 +1,107 @@
<template>
<DashboardWidget :items="items"
:loading="loading"
>
<template #default="{ item }">
<DashboardWidgetItem
:target-url="getItemTargetUrl(item)"
:main-text="item.title"
:sub-text="item.content"
>
<template #avatar>
<div
class="note-item"
:class="{ 'icon-pinned': item.pinned, 'note-item-no-pinned': !hasPinned }"
/>
</template>
</DashboardWidgetItem>
</template>
<template #empty-content>
<EmptyContent icon="icon-quicknotes">
<template #desc>
<p class="notes-empty-content-label">
{{ t('quicknotes', 'Nothing here. Take your first quick notes') }}
</p>
<p>
<a :href="createNoteUrl" class="button">{{ t('quicknotes', 'Create a 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 } from '../NotesService.js'
export default {
name: 'Dashboard',
components: {
DashboardWidget,
DashboardWidgetItem,
EmptyContent,
},
data() {
return {
loading: true,
items: [],
}
},
computed: {
hasPinned() {
return this.items.length > 0 && this.items[0].pinned
},
createNoteUrl() {
return generateUrl('/apps/quicknotes')
},
getItemTargetUrl() {
return (note) => {
return generateUrl('/apps/quicknotes/?n=' + note.id)
}
},
},
created() {
this.loadDashboardData()
},
methods: {
loadDashboardData() {
getDashboardData().then(data => {
this.items = data.notes
this.loading = false
})
},
},
}
</script>
<style scoped>
.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-pinned {
display: none;
}
.notes-empty-content-label {
margin-bottom: 20px;
}
</style>

11
src/dashboard.js Normal file
View File

@@ -0,0 +1,11 @@
import Vue from 'vue'
import Dashboard from './components/Dashboard.vue'
Vue.mixin({ methods: { t, n } })
document.addEventListener('DOMContentLoaded', () => {
OCA.Dashboard.register('quicknotes', (el) => {
const View = Vue.extend(Dashboard)
new View().$mount(el)
})
})

8
webpack.config.js Normal file
View File

@@ -0,0 +1,8 @@
const path = require('path')
const webpackConfig = require('@nextcloud/webpack-vue-config')
webpackConfig.entry = {
dashboard: { import: path.join(__dirname, 'src', 'dashboard.js') }
}
module.exports = webpackConfig