Merge pull request #3388 from nextcloud/enh/dashboard

Implement dashboard panel
This commit is contained in:
Christoph Wurst 2020-08-10 18:42:23 +02:00 коммит произвёл GitHub
Родитель 1530f48e5a e7ab9f296c
Коммит acdf45fbba
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 440 добавлений и 0 удалений

1
img/newsletter.svg Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Просмотреть файл

@ -41,6 +41,7 @@ use OCA\Mail\Events\SaveDraftEvent;
use OCA\Mail\Http\Middleware\ErrorMiddleware;
use OCA\Mail\Http\Middleware\ProvisioningMiddleware;
use OCA\Mail\Listener\AddressCollectionListener;
use OCA\Mail\Listener\DashboardPanelListener;
use OCA\Mail\Listener\DeleteDraftListener;
use OCA\Mail\Listener\DraftMailboxCreatorListener;
use OCA\Mail\Listener\FlagRepliedMessageListener;
@ -59,6 +60,7 @@ use OCA\Mail\Service\Search\MailSearch;
use OCA\Mail\Service\UserPreferenceSevice;
use OCP\AppFramework\App;
use OCP\AppFramework\IAppContainer;
use OCP\Dashboard\RegisterWidgetEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
@ -115,5 +117,6 @@ class Application extends App {
$dispatcher->addServiceListener(SaveDraftEvent::class, DraftMailboxCreatorListener::class);
$dispatcher->addServiceListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class);
$dispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedListener::class);
$dispatcher->addServiceListener(RegisterWidgetEvent::class, DashboardPanelListener::class);
}
}

Просмотреть файл

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
namespace OCA\Mail\Dashboard;
use OCA\Mail\AppInfo\Application;
use OCA\Mail\Service\AccountService;
use OCP\AppFramework\Services\IInitialState;
use OCP\Dashboard\IWidget;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Util;
class MailWidget implements IWidget {
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
/** @var AccountService */
private $accountService;
/** @var IInitialState */
private $initialState;
/** @var string|null */
private $userId;
public function __construct(IL10N $l10n,
IURLGenerator $urlGenerator,
AccountService $accountService,
IInitialState $initialState,
?string $userId) {
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
$this->accountService = $accountService;
$this->initialState = $initialState;
$this->userId = $userId;
}
/**
* @inheritDoc
*/
public function getId(): string {
return Application::APP_ID;
}
/**
* @inheritDoc
*/
public function getTitle(): string {
return $this->l10n->t('Important mail');
}
/**
* @inheritDoc
*/
public function getOrder(): int {
return 4;
}
/**
* @inheritDoc
*/
public function getIconClass(): string {
return 'icon-mail';
}
/**
* @inheritDoc
*/
public function getUrl(): ?string {
return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('mail.page.index'));
}
/**
* @inheritDoc
*/
public function load(): void {
Util::addScript(Application::APP_ID, 'dashboard');
$this->initialState->provideInitialState(
'mail-accounts',
$this->accountService->findByUserId($this->userId)
);
}
}

Просмотреть файл

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
namespace OCA\Mail\Listener;
use OCA\Mail\Dashboard\MailWidget;
use OCP\Dashboard\RegisterWidgetEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
class DashboardPanelListener implements IEventListener {
/**
* @inheritDoc
*/
public function handle(Event $event): void {
if (!($event instanceof RegisterWidgetEvent)) {
return;
}
$event->registerWidget(MailWidget::class);
}
}

104
package-lock.json сгенерированный
Просмотреть файл

@ -953,6 +953,27 @@
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"@babel/polyfill": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.2.5.tgz",
"integrity": "sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==",
"requires": {
"core-js": "^2.5.7",
"regenerator-runtime": "^0.12.0"
},
"dependencies": {
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"@babel/preset-env": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz",
@ -1657,6 +1678,44 @@
"vue2-datepicker": "^3.3.1"
}
},
"@nextcloud/vue-dashboard": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@nextcloud/vue-dashboard/-/vue-dashboard-0.1.3.tgz",
"integrity": "sha512-7b02zkarX7b18IRQmZEW1NM+dvtcUih2M0+CZyuQfcvfyMQudOz+BdA/oD1p7PmdBds1IR8OvY1+CnpmgAzfQg==",
"requires": {
"@nextcloud/vue": "^2.3.0",
"core-js": "^3.6.4",
"vue": "^2.6.11"
},
"dependencies": {
"@nextcloud/vue": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-2.3.0.tgz",
"integrity": "sha512-6uf7Hu4Obaet7BOs9H/Ng63xAYqks9CL7hsOOHGUzWFYrPPBxgt79iD9OOPpPfJuLQ3Nnuibh942X1QreCBRkw==",
"requires": {
"@nextcloud/auth": "^1.2.3",
"@nextcloud/axios": "^1.3.2",
"@nextcloud/dialogs": "^1.3.0",
"@nextcloud/event-bus": "^1.1.4",
"@nextcloud/l10n": "^1.2.3",
"@nextcloud/router": "^1.0.2",
"core-js": "^3.6.5",
"debounce": "1.2.0",
"emoji-mart-vue-fast": "^7.0.2",
"hammerjs": "^2.0.8",
"md5": "^2.2.1",
"regenerator-runtime": "^0.13.5",
"v-click-outside": "^3.0.1",
"v-tooltip": "^2.0.3",
"vue": "^2.6.11",
"vue-color": "^2.7.1",
"vue-multiselect": "^2.1.6",
"vue-visible": "^1.0.2",
"vue2-datepicker": "^3.4.1"
}
}
}
},
"@nodelib/fs.scandir": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
@ -4695,6 +4754,31 @@
}
}
},
"emoji-mart-vue-fast": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-mart-vue-fast/-/emoji-mart-vue-fast-7.0.3.tgz",
"integrity": "sha512-9BdX1QvWCOKEnmd20wcej7GaB/2/cesgodGJCCQirz1NtW3xctg1pWEYJHbAcjRHSHDzLDC+Y2xj9a2tO8T5hQ==",
"requires": {
"@babel/polyfill": "7.2.5",
"@babel/runtime": "7.3.4",
"vue-virtual-scroller": "^1.0.0-rc.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
"requires": {
"regenerator-runtime": "^0.12.0"
}
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -10926,6 +11010,11 @@
"ajv-keywords": "^3.1.0"
}
},
"scrollparent": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.0.1.tgz",
"integrity": "sha1-cV1bnMV3YPsivczDvvtb/gaxoxc="
},
"scss-tokenizer": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
@ -12676,6 +12765,11 @@
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz",
"integrity": "sha512-s7jmZPlm9FeueJg1RwJtnE9KNPtME/7C8uRWSfp9/yEN4M8XcS/d+bddoyVwVnvFyRh9msFo0HWeW0vTL8Qv+w=="
},
"vue-observe-visibility": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
"integrity": "sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q=="
},
"vue-on-click-outside": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/vue-on-click-outside/-/vue-on-click-outside-1.0.3.tgz",
@ -12790,6 +12884,16 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vue-virtual-scroller": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.0.10.tgz",
"integrity": "sha512-Hn4qSBDhRY4XdngPioYy/ykDjrLX/NMm1fQXm/4UQQ/Xv1x8JbHGFZNftQowTcfICgN7yc31AKnUk1UGLJ2ndA==",
"requires": {
"scrollparent": "^2.0.1",
"vue-observe-visibility": "^0.4.4",
"vue-resize": "^0.4.5"
}
},
"vue-visible": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/vue-visible/-/vue-visible-1.0.2.tgz",

Просмотреть файл

@ -39,6 +39,7 @@
"@nextcloud/moment": "^1.1.1",
"@nextcloud/router": "^1.1.0",
"@nextcloud/vue": "^1.5.0",
"@nextcloud/vue-dashboard": "^0.1.3",
"@vue/babel-preset-app": "^4.4.6",
"color-convert": "^2.0.1",
"core-js": "^3.6.5",

44
src/main-dashboard.js Normal file
Просмотреть файл

@ -0,0 +1,44 @@
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
import Vue from 'vue'
import { getRequestToken } from '@nextcloud/auth'
import { generateFilePath } from '@nextcloud/router'
import Nextcloud from './mixins/Nextcloud'
import Dashboard from './views/Dashboard'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())
// eslint-disable-next-line camelcase
__webpack_public_path__ = generateFilePath('mail', '', 'js/')
Vue.mixin(Nextcloud)
document.addEventListener('DOMContentLoaded', function() {
const register = OCA?.Dashboard?.register || (() => {})
register('mail', (el) => {
const View = Vue.extend(Dashboard)
new View().$mount(el)
})
})

126
src/views/Dashboard.vue Normal file
Просмотреть файл

@ -0,0 +1,126 @@
<!--
- @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- 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/>.
-
-->
<template>
<DashboardWidget :items="importantMessages"
:show-more-url="''"
:loading="loading"
@hide="() => {}"
@markDone="() => {}">
<template v-slot:default="{ item }">
<DashboardWidgetItem :item="getWidgetItem(item)">
<template v-slot:avatar>
<Avatar v-if="item.from" :email="item.from[0].email" :display-name="item.from[0].label" />
</template>
</DashboardWidgetItem>
</template>
<template v-slot:empty-content>
<div class="empty-content">
<img class="empty-content__image" :src="emptyImage">
<p class="empty-content__text">
{{ t('mail', 'No messages found yet') }}
</p>
<a v-if="accounts.length === 2" :href="accountSetupUrl" class="button">{{ t('mail', 'Set up an account') }}</a>
</div>
</template>
</DashboardWidget>
</template>
<script>
import { fetchEnvelopes } from '../service/MessageService'
import { loadState } from '@nextcloud/initial-state'
import { generateUrl, imagePath } from '@nextcloud/router'
import Avatar from '../components/Avatar'
import { DashboardWidget, DashboardWidgetItem } from '@nextcloud/vue-dashboard'
import orderBy from 'lodash/fp/orderBy'
import prop from 'lodash/fp/prop'
const accounts = loadState('mail', 'mail-accounts')
const orderByDateInt = orderBy(prop('dateInt'), 'desc')
export default {
name: 'Dashboard',
components: {
Avatar,
DashboardWidget,
DashboardWidgetItem,
},
data() {
return {
messages: [],
accounts,
fetchedAccounts: 0,
emptyImage: imagePath('mail', 'newsletter.svg'),
accountSetupUrl: generateUrl('/apps/mail/#/setup'),
}
},
computed: {
loading() {
return this.fetchedAccounts < this.accounts.length
},
importantMessages() {
if (!this.messages) {
return []
}
return orderByDateInt(this.messages).slice(0, 7)
},
getWidgetItem() {
return (item) => {
const { uid, accountId, mailbox } = item
return {
targetUrl: generateUrl(`/apps/mail/#/accounts/${accountId}/folders/${mailbox}/message/${accountId}-${mailbox}-${uid}`),
mainText: item.from ? item.from[0].label : '',
subText: item.subject,
message: item,
}
}
},
},
mounted() {
// TODO: check if there is a more sane way to query this and if other mailboxes should be fetched as well
this.accounts.forEach((account) => {
fetchEnvelopes(account.accountId, btoa('INBOX'), 'is:important', undefined, 10).then((messages) => {
messages = messages.map((message) => ({ ...message, accountId: account.accountId, mailbox: btoa('INBOX') }))
this.messages = this.messages !== null ? [...this.messages, ...messages] : messages
this.fetchedAccounts++
})
})
},
}
</script>
<style lang="scss" scoped>
.empty-content {
text-align: center;
margin-top: 50px;
}
.empty-content__image {
width: 80%;
margin: auto;
margin-bottom: 10px;
}
.empty-content__text {
color: var(--color-text-maxcontrast);
text-align: center;
margin-bottom: 10px;
}
</style>

Просмотреть файл

@ -20,6 +20,7 @@ if (process.env.BUNDLE_ANALYZER_TOKEN) {
module.exports = {
entry: {
autoredirect: path.join(__dirname, 'src/autoredirect.js'),
dashboard: path.join(__dirname, 'src/main-dashboard.js'),
mail: path.join(__dirname, 'src/main.js'),
settings: path.join(__dirname, 'src/main-settings')
},