]> source.dussan.org Git - nextcloud-server.git/commitdiff
Allow userdefined order and start with drag and drop resorting
authorJulius Härtl <jus@bitgrid.net>
Mon, 15 Jun 2020 06:18:50 +0000 (08:18 +0200)
committerJulius Härtl <jus@bitgrid.net>
Wed, 5 Aug 2020 15:01:27 +0000 (17:01 +0200)
Signed-off-by: Julius Härtl <jus@bitgrid.net>
apps/dashboard/appinfo/routes.php
apps/dashboard/lib/Controller/DashboardController.php
apps/dashboard/src/App.vue
apps/dashboard/src/main.js
package-lock.json
package.json

index 34792a9d47dbd1cb367a849a8c03de03643c2577..4edca1a3ec56d966a5bc6d61ced61fc5bbf01b28 100644 (file)
@@ -27,5 +27,6 @@ declare(strict_types=1);
 return [
        'routes' => [
                ['name' => 'dashboard#index', 'url' => '/', 'verb' => 'GET'],
+               ['name' => 'dashboard#updateLayout', 'url' => '/layout', 'verb' => 'POST'],
        ]
 ];
index 687fbace380d04a1226d52aae298a994c7f4609f..e796ae67ccd0697084db124ed61c69c81f7b7910 100644 (file)
@@ -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]);
+       }
 }
index 87c76a603b43496370b326606cf74b6037ec4975..44cb763b020bc4e0fe60d7bc728e3024fdcdd17f 100644 (file)
@@ -2,16 +2,41 @@
        <div id="app-dashboard">
                <h2>{{ greeting.icon }} {{ greeting.text }}</h2>
 
-               <div class="panels">
-                       <div v-for="panel in panels" :key="panel.id" class="panel">
-                               <a :href="panel.url">
-                                       <h3 :class="panel.iconClass">
-                                               {{ panel.title }}
-                                       </h3>
-                               </a>
-                               <div :ref="panel.id" :data-id="panel.id" />
+               <Container class="panels"
+                       orientation="horizontal"
+                       drag-handle-selector=".panel--header"
+                       @drop="onDrop">
+                       <Draggable v-for="panelId in layout" :key="panels[panelId].id" class="panel">
+                               <div class="panel--header">
+                                       <a :href="panels[panelId].url">
+                                               <h3 :class="panels[panelId].iconClass">
+                                                       {{ panels[panelId].title }}
+                                               </h3>
+                                       </a>
+                               </div>
+                               <div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
+                       </Draggable>
+               </Container>
+               <a class="add-panels icon-add" @click="showModal">Add more panels</a>
+               <Modal v-if="modal" @close="closeModal">
+                       <div class="modal__content">
+                               <transition-group name="flip-list" tag="ol">
+                                       <li v-for="panel in sortedPanels" :key="panel.id">
+                                               <input :id="'panel-checkbox-' + panel.id"
+                                                       type="checkbox"
+                                                       class="checkbox"
+                                                       :checked="isActive(panel)"
+                                                       @input="updateCheckbox(panel, $event.target.checked)">
+                                               <label :for="'panel-checkbox-' + panel.id">
+                                                       {{ panel.title }}
+                                               </label>
+                                       </li>
+                                       <li key="appstore">
+                                               <a href="/index.php/apps/settings" class="button">{{ t('dashboard', 'Get more panels from the app store') }}</a>
+                                       </li>
+                               </transition-group>
                        </div>
-               </div>
+               </Modal>
        </div>
 </template>
 
 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,15 +104,46 @@ 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() {
+                       this.rerenderPanels()
+               },
+       },
+       mounted() {
+               setInterval(() => {
+                       this.timer = new Date()
+               }, 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].mounted) {
+                               if (this.panels[app] && this.panels[app].mounted) {
                                        continue
                                }
-
                                if (element) {
                                        this.callbacks[app](element[0])
                                        Vue.set(this.panels[app], 'mounted', true)
@@ -67,15 +152,33 @@ export default {
                                }
                        }
                },
-       },
-       mounted() {
-               setInterval(() => {
-                       this.timer = new Date()
-               }, 30000)
-       },
-       methods: {
-               register(app, callback) {
-                       Vue.set(this.callbacks, app, callback)
+
+               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;
+       }
+
 </style>
index 998f538356bc7df7582a6e7ea6d1cabc2603c9e5..e1c2c59a10f22b14b32d3a2bbc6a2ca68ce7b91f 100644 (file)
@@ -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')
index ab643d9beaa63718925b4b974add30841902fb79..a4715c500eb6ccb93dc455febc775ce06b8379c3 100644 (file)
         "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",
       "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",
index e8487b19d2c95ab76b201702277f9896761afbb8..db2528e21f057bcd7091deea675402846e8a2eb8 100644 (file)
@@ -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"
   },