From 429049c809226f3750647a19a4cb48e0d3d4ea75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 15 Jun 2020 08:18:50 +0200 Subject: [PATCH] Allow userdefined order and start with drag and drop resorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/dashboard/appinfo/routes.php | 1 + .../lib/Controller/DashboardController.php | 28 ++- apps/dashboard/src/App.vue | 200 +++++++++++++++--- apps/dashboard/src/main.js | 2 + package-lock.json | 13 ++ package.json | 1 + 6 files changed, 215 insertions(+), 30 deletions(-) diff --git a/apps/dashboard/appinfo/routes.php b/apps/dashboard/appinfo/routes.php index 34792a9d47..4edca1a3ec 100644 --- a/apps/dashboard/appinfo/routes.php +++ b/apps/dashboard/appinfo/routes.php @@ -27,5 +27,6 @@ declare(strict_types=1); return [ 'routes' => [ ['name' => 'dashboard#index', 'url' => '/', 'verb' => 'GET'], + ['name' => 'dashboard#updateLayout', 'url' => '/layout', 'verb' => 'POST'], ] ]; diff --git a/apps/dashboard/lib/Controller/DashboardController.php b/apps/dashboard/lib/Controller/DashboardController.php index 687fbace38..e796ae67cc 100644 --- a/apps/dashboard/lib/Controller/DashboardController.php +++ b/apps/dashboard/lib/Controller/DashboardController.php @@ -26,13 +26,16 @@ declare(strict_types=1); namespace OCA\Dashboard\Controller; +use OCA\Dashboard\AppInfo\Application; use OCA\Viewer\Event\LoadViewer; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\Dashboard\IManager; use OCP\Dashboard\IPanel; use OCP\Dashboard\RegisterPanelEvent; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IInitialStateService; use OCP\IRequest; @@ -44,19 +47,27 @@ class DashboardController extends Controller { private $eventDispatcher; /** @var IManager */ private $dashboardManager; + /** @var IConfig */ + private $config; + /** @var string */ + private $userId; public function __construct( string $appName, IRequest $request, IInitialStateService $initialStateService, IEventDispatcher $eventDispatcher, - IManager $dashboardManager + IManager $dashboardManager, + IConfig $config, + $userId ) { parent::__construct($appName, $request); $this->inititalStateService = $initialStateService; $this->eventDispatcher = $eventDispatcher; $this->dashboardManager = $dashboardManager; + $this->config = $config; + $this->userId = $userId; } /** @@ -67,7 +78,7 @@ class DashboardController extends Controller { public function index(): TemplateResponse { $this->eventDispatcher->dispatchTyped(new RegisterPanelEvent($this->dashboardManager)); - $dashboardManager = $this->dashboardManager; + $userLayout = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', 'calendar,recommendations,spreed,mail')); $panels = array_map(function (IPanel $panel) { return [ 'id' => $panel->getId(), @@ -75,8 +86,9 @@ class DashboardController extends Controller { 'iconClass' => $panel->getIconClass(), 'url' => $panel->getUrl() ]; - }, $dashboardManager->getPanels()); + }, $this->dashboardManager->getPanels()); $this->inititalStateService->provideInitialState('dashboard', 'panels', $panels); + $this->inititalStateService->provideInitialState('dashboard', 'layout', $userLayout); if (class_exists(LoadViewer::class)) { $this->eventDispatcher->dispatchTyped(new LoadViewer()); @@ -84,4 +96,14 @@ class DashboardController extends Controller { return new TemplateResponse('dashboard', 'index'); } + + /** + * @NoAdminRequired + * @param string $layout + * @return JSONResponse + */ + public function updateLayout(string $layout): JSONResponse { + $this->config->setUserValue($this->userId, 'dashboard', 'layout', $layout); + return new JSONResponse(['layout' => $layout]); + } } diff --git a/apps/dashboard/src/App.vue b/apps/dashboard/src/App.vue index 87c76a603b..44cb763b02 100644 --- a/apps/dashboard/src/App.vue +++ b/apps/dashboard/src/App.vue @@ -2,16 +2,41 @@

{{ greeting.icon }} {{ greeting.text }}

-
-
- -

- {{ panel.title }} -

-
- @@ -19,17 +44,46 @@ import Vue from 'vue' import { loadState } from '@nextcloud/initial-state' import { getCurrentUser } from '@nextcloud/auth' +import { Modal } from '@nextcloud/vue' +import { Container, Draggable } from 'vue-smooth-dnd' +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' const panels = loadState('dashboard', 'panels') +const applyDrag = (arr, dragResult) => { + const { removedIndex, addedIndex, payload } = dragResult + if (removedIndex === null && addedIndex === null) return arr + + const result = [...arr] + let itemToAdd = payload + + if (removedIndex !== null) { + itemToAdd = result.splice(removedIndex, 1)[0] + } + + if (addedIndex !== null) { + result.splice(addedIndex, 0, itemToAdd) + } + + return result +} + export default { name: 'App', + components: { + Modal, + Container, + Draggable, + }, data() { return { timer: new Date(), callbacks: {}, panels, name: getCurrentUser()?.displayName, + layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]), + modal: false, } }, computed: { @@ -50,22 +104,23 @@ export default { } return { icon: '🦉', text: t('dashboard', 'Have a night owl, {name}', { name: this.name }) } }, + isActive() { + return (panel) => this.layout.indexOf(panel.id) > -1 + }, + sortedPanels() { + return Object.values(this.panels).sort((a, b) => { + const indexA = this.layout.indexOf(a.id) + const indexB = this.layout.indexOf(b.id) + if (indexA === -1 || indexB === -1) { + return indexB - indexA || a.id - b.id + } + return indexA - indexB || a.id - b.id + }) + }, }, watch: { callbacks() { - for (const app in this.callbacks) { - const element = this.$refs[app] - if (this.panels[app].mounted) { - continue - } - - if (element) { - this.callbacks[app](element[0]) - Vue.set(this.panels[app], 'mounted', true) - } else { - console.error('Failed to register panel in the frontend as no backend data was provided for ' + app) - } - } + this.rerenderPanels() }, }, mounted() { @@ -74,9 +129,57 @@ export default { }, 30000) }, methods: { + /** + * Method to register panels that will be called by the integrating apps + * + * @param {string} app The unique app id for the widget + * @param {function} callback The callback function to register a panel which gets the DOM element passed as parameter + */ register(app, callback) { Vue.set(this.callbacks, app, callback) }, + rerenderPanels() { + for (const app in this.callbacks) { + const element = this.$refs[app] + if (this.panels[app] && this.panels[app].mounted) { + continue + } + if (element) { + this.callbacks[app](element[0]) + Vue.set(this.panels[app], 'mounted', true) + } else { + console.error('Failed to register panel in the frontend as no backend data was provided for ' + app) + } + } + }, + + saveLayout() { + axios.post(generateUrl('/apps/dashboard/layout'), { + layout: this.layout.join(','), + }) + }, + onDrop(dropResult) { + this.layout = applyDrag(this.layout, dropResult) + this.saveLayout() + }, + showModal() { + this.modal = true + }, + closeModal() { + this.modal = false + }, + updateCheckbox(panel, currentValue) { + const index = this.layout.indexOf(panel.id) + if (!currentValue && index > -1) { + this.layout.splice(index, 1) + + } else { + this.layout.push(panel.id) + } + Vue.set(this.panels[panel.id], 'mounted', false) + this.saveLayout() + this.$nextTick(() => this.rerenderPanels()) + }, }, } @@ -101,18 +204,30 @@ export default { flex-wrap: wrap; } - .panel { - width: 250px; - margin: 16px; + .panel, .panels > div { + width: 280px; + padding: 16px; - & > a { + .panel--header h3 { + cursor: grab; + &:active { + cursor: grabbing; + } + } + + & > .panel--header { position: sticky; top: 50px; - display: block; background: linear-gradient(var(--color-main-background-translucent), var(--color-main-background-translucent) 80%, rgba(255, 255, 255, 0)); backdrop-filter: blur(4px); + display: flex; + a { + flex-grow: 1; + } h3 { + display: block; + flex-grow: 1; margin: 0; font-size: 20px; font-weight: bold; @@ -123,4 +238,35 @@ export default { } } + .add-panels { + position: fixed; + bottom: 20px; + right: 20px; + padding: 10px; + padding-left: 35px; + padding-right: 15px; + background-position: 10px center; + border-radius: 100px; + &:hover { + background-color: var(--color-background-hover); + } + } + + .modal__content { + width: 30vw; + margin: 20px; + ol { + list-style-type: none; + } + li label { + padding: 10px; + display: block; + list-style-type: none; + } + } + + .flip-list-move { + transition: transform 1s; + } + diff --git a/apps/dashboard/src/main.js b/apps/dashboard/src/main.js index 998f538356..e1c2c59a10 100644 --- a/apps/dashboard/src/main.js +++ b/apps/dashboard/src/main.js @@ -1,5 +1,7 @@ import Vue from 'vue' import App from './App.vue' +import { translate as t } from '@nextcloud/l10n' +Vue.prototype.t = t const Dashboard = Vue.extend(App) const Instance = new Dashboard({}).$mount('#app') diff --git a/package-lock.json b/package-lock.json index ab643d9bea..a4715c500e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8798,6 +8798,11 @@ "is-fullwidth-code-point": "^2.0.0" } }, + "smooth-dnd": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/smooth-dnd/-/smooth-dnd-0.12.1.tgz", + "integrity": "sha512-Dndj/MOG7VP83mvzfGCLGzV2HuK1lWachMtWl/Iuk6zV7noDycIBnflwaPuDzoaapEl3Pc4+ybJArkkx9sxPZg==" + }, "snap.js": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/snap.js/-/snap.js-2.0.9.tgz", @@ -9913,6 +9918,14 @@ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.3.4.tgz", "integrity": "sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg==" }, + "vue-smooth-dnd": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/vue-smooth-dnd/-/vue-smooth-dnd-0.8.1.tgz", + "integrity": "sha512-eZVVPTwz4A1cs0+CjXx/ihV+gAl3QBoWQnU6+23Gp59t0WBU99z7ducBQ4FvjBamqOlg8SDOE5eFHQedxwB4Wg==", + "requires": { + "smooth-dnd": "0.12.1" + } + }, "vue-style-loader": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", diff --git a/package.json b/package.json index e8487b19d2..db2528e21f 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "vue-material-design-icons": "^4.8.0", "vue-multiselect": "^2.1.6", "vue-router": "^3.3.4", + "vue-smooth-dnd": "^0.8.1", "vuex": "^3.5.1", "vuex-router-sync": "^5.0.0" },