Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
429049c
Allow userdefined order and start with drag and drop resorting
juliusknorr Jun 15, 2020
28a01a9
Design enhancements to panels, headings, and edit button
jancborchardt Jul 10, 2020
9cde198
More design fixes to dragging and edit panels
jancborchardt Jul 10, 2020
1448f54
Experiment with fade in dashboard panel header
jancborchardt Jul 10, 2020
0135eed
Dashboard: Adjust headings to new spacing for 44px icons
jancborchardt Jul 11, 2020
90f56dd
Replace vue-smoothdnd with vuedraggable
juliusknorr Jul 22, 2020
e25bab9
Set Dashboard as default app
jancborchardt Jul 21, 2020
4679926
Redirect to files app after login in acceptance tests
juliusknorr Jul 24, 2020
995144f
Dashboard: Fix small misalignment of widget header icon
jancborchardt Jul 31, 2020
da8a29f
Dashboard: Wording change from panels to widgets
jancborchardt Jul 31, 2020
5099a4f
Add binary attributes for dashboard bundles
juliusknorr Jul 27, 2020
c983094
WIP: drag and drop in modal
juliusknorr Jul 27, 2020
bd3d791
Only show display name if set
juliusknorr Jul 31, 2020
ae6be0c
Expose firstRun parameter to frontend
juliusknorr Jul 31, 2020
3be3c34
Status integration
juliusknorr Aug 4, 2020
7e2ded5
Fix default order of widgets
juliusknorr Aug 4, 2020
db86bea
Allow default app to be overwritten by user config
juliusknorr Aug 4, 2020
6accf4d
Fix default height
juliusknorr Aug 4, 2020
018be66
Refactor API to match the widget wording
juliusknorr Aug 4, 2020
d9dcd59
Load sidebar on dashboard
juliusknorr Aug 5, 2020
97feb89
Make header the drag handler
juliusknorr Aug 5, 2020
c97a805
Add first run hint
juliusknorr Aug 5, 2020
7399146
Dashboard: Fix full height of Widgets based on new component
jancborchardt Aug 4, 2020
44310d1
Add dashboard to app info xsd
juliusknorr Aug 5, 2020
61cc356
Fix php cs check
juliusknorr Aug 5, 2020
dbbc675
Fix drag behavior in modal
juliusknorr Aug 5, 2020
258cde1
Bump bundles
juliusknorr Aug 5, 2020
5a06e38
Dashboard app is disabled and there is no need to redirect to files app
MorrisJobke Aug 5, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Allow userdefined order and start with drag and drop resorting
Signed-off-by: Julius Härtl <[email protected]>
  • Loading branch information
juliusknorr committed Aug 5, 2020
commit 429049c809226f3750647a19a4cb48e0d3d4ea75
1 change: 1 addition & 0 deletions apps/dashboard/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
return [
'routes' => [
['name' => 'dashboard#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'dashboard#updateLayout', 'url' => '/layout', 'verb' => 'POST'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be more compliant to what REST is about I would suggest to restructure this to be a resource, like simply /dashboard/layout and POST the data there

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd look into this in a separate pr since my first attempt for that didn't work out as expected.

]
];
28 changes: 25 additions & 3 deletions apps/dashboard/lib/Controller/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@

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;

Expand All @@ -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;
}

/**
Expand All @@ -67,21 +78,32 @@ public function __construct(
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(),
'title' => $panel->getTitle(),
'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());
}

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]);
}
}
196 changes: 171 additions & 25 deletions apps/dashboard/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,88 @@
<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>

<script>
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: {
Expand All @@ -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)
Expand All @@ -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())
},
},
}
Expand All @@ -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;
Expand All @@ -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>
2 changes: 2 additions & 0 deletions apps/dashboard/src/main.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down