Skip to content

Commit c482e40

Browse files
julien-ncsusnux
authored andcommitted
chore: migrate the dashboard app to vue3
- adjust to new vuedraggable - fix #app-content-vue width - close dashboard settings modal on click outside - fix eslint issues - remove dashboard app symlink in build/frontend-legacy/apps add it in build/frontend/apps Assisted-by: OpenCode:minimax-m3 Signed-off-by: Julien Veyssier <julien-nc@posteo.net>
1 parent 9abf97b commit c482e40

11 files changed

Lines changed: 176 additions & 110 deletions

File tree

apps/dashboard/lib/Controller/DashboardController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
1616
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
1717
use OCP\AppFramework\Http\Attribute\OpenAPI;
18+
use OCP\AppFramework\Http\ContentSecurityPolicy;
1819
use OCP\AppFramework\Http\FeaturePolicy;
1920
use OCP\AppFramework\Http\TemplateResponse;
2021
use OCP\AppFramework\Services\IInitialState;
@@ -51,6 +52,7 @@ public function __construct(
5152
#[NoAdminRequired]
5253
#[FrontpageRoute(verb: 'GET', url: '/')]
5354
public function index(): TemplateResponse {
55+
Util::addStyle('dashboard', 'main');
5456
Util::addStyle('dashboard', 'dashboard');
5557
Util::addScript('dashboard', 'main', 'theming');
5658

@@ -78,6 +80,10 @@ public function index(): TemplateResponse {
7880
'pageTitle' => $this->l10n->t('Dashboard'),
7981
]);
8082

83+
$csp = new ContentSecurityPolicy();
84+
$csp->addAllowedScriptDomain("'unsafe-eval'");
85+
$response->setContentSecurityPolicy($csp);
86+
8187
// For the weather widget we should allow the geolocation
8288
$featurePolicy = new FeaturePolicy();
8389
$featurePolicy->addAllowedGeoLocationDomain('\'self\'');

apps/dashboard/src/DashboardApp.vue

Lines changed: 133 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,45 @@
1010
v-for="status in sortedRegisteredStatus"
1111
:id="'status-' + status"
1212
:key="status">
13-
<div :ref="'status-' + status" />
13+
<div :ref="(element) => setStatusElement(status, element)" />
1414
</li>
1515
</ul>
1616

1717
<Draggable
1818
v-model="layout"
19-
class="panels"
2019
v-bind="{ swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3 }"
20+
class="panels"
21+
:itemKey="(element) => element"
2122
handle=".panel--header"
2223
@end="saveLayout">
23-
<template v-for="panelId in layout">
24-
<div
25-
v-if="isApiWidgetV2(panels[panelId].id)"
26-
:key="`${panels[panelId].id}-v2`"
27-
class="panel">
28-
<div class="panel--header">
29-
<h2>
30-
<img v-if="apiWidgets[panels[panelId].id].icon_url" :src="apiWidgets[panels[panelId].id].icon_url" alt="">
31-
<span v-else :class="apiWidgets[panels[panelId].id].icon_class" aria-hidden="true" />
32-
{{ apiWidgets[panels[panelId].id].title }}
33-
</h2>
34-
</div>
35-
<div class="panel--content">
36-
<ApiDashboardWidget
37-
:widget="apiWidgets[panels[panelId].id]"
38-
:data="apiWidgetItems[panels[panelId].id]"
39-
:loading="loadingItems" />
40-
</div>
41-
</div>
42-
<div v-else :key="panels[panelId].id" class="panel">
43-
<div class="panel--header">
44-
<h2>
45-
<span :class="panels[panelId].iconClass" aria-hidden="true" />
46-
{{ panels[panelId].title }}
47-
</h2>
48-
</div>
49-
<div class="panel--content" :class="{ loading: !panels[panelId].mounted }">
50-
<div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
51-
</div>
24+
<template #item="{ element: panelId }">
25+
<div :key="panelId" class="panel">
26+
<template v-if="isApiWidgetV2(panels[panelId].id)">
27+
<div class="panel--header">
28+
<h2>
29+
<img v-if="apiWidgets[panels[panelId].id].icon_url" :src="apiWidgets[panels[panelId].id].icon_url" alt="">
30+
<span v-else :class="apiWidgets[panels[panelId].id].icon_class" aria-hidden="true" />
31+
{{ apiWidgets[panels[panelId].id].title }}
32+
</h2>
33+
</div>
34+
<div class="panel--content">
35+
<ApiDashboardWidget
36+
:widget="apiWidgets[panels[panelId].id]"
37+
:data="apiWidgetItems[panels[panelId].id]"
38+
:loading="loadingItems" />
39+
</div>
40+
</template>
41+
<template v-else>
42+
<div class="panel--header">
43+
<h2>
44+
<span :class="panels[panelId].iconClass" aria-hidden="true" />
45+
{{ panels[panelId].title }}
46+
</h2>
47+
</div>
48+
<div class="panel--content" :class="{ loading: !panels[panelId].mounted }">
49+
<div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
50+
</div>
51+
</template>
5252
</div>
5353
</template>
5454
</Draggable>
@@ -62,7 +62,11 @@
6262
</NcButton>
6363
</div>
6464

65-
<NcModal v-if="modal" size="large" @close="closeModal">
65+
<NcModal
66+
v-if="modal"
67+
size="large"
68+
:closeOnClickOutside="true"
69+
@close="closeModal">
6670
<div class="modal__content">
6771
<h2>{{ t('dashboard', 'Edit widgets') }}</h2>
6872
<ol class="panels">
@@ -81,27 +85,31 @@
8185
</li>
8286
</ol>
8387
<Draggable
84-
v-model="layout"
88+
v-bind="{ swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3 }"
8589
class="panels"
8690
tag="ol"
87-
v-bind="{ swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3 }"
91+
:itemKey="(panel) => panel.id"
8892
handle=".draggable"
89-
@end="saveLayout">
90-
<li v-for="panel in sortedPanels" :key="panel.id" :class="'panel-' + panel.id">
91-
<input
92-
:id="'panel-checkbox-' + panel.id"
93-
type="checkbox"
94-
class="checkbox"
95-
:checked="isActive(panel)"
96-
@input="updateCheckbox(panel, $event.target.checked)">
97-
<label :for="'panel-checkbox-' + panel.id" :class="{ draggable: isActive(panel) }">
98-
<img v-if="panel.iconUrl" alt="" :src="panel.iconUrl">
99-
<span v-else :class="panel.iconClass" aria-hidden="true" />
100-
{{ panel.title }}
101-
</label>
102-
</li>
93+
:modelValue="modalPanelList"
94+
@update:modelValue="onModalPanelListUpdate"
95+
@start="onModalDragStart"
96+
@end="onModalDragEnd">
97+
<template #item="{ element: panel }">
98+
<li :key="panel.id" :class="'panel-' + panel.id">
99+
<input
100+
:id="'panel-checkbox-' + panel.id"
101+
type="checkbox"
102+
class="checkbox"
103+
:checked="isActive(panel)"
104+
@input="updateCheckbox(panel, $event.target.checked)">
105+
<label :for="'panel-checkbox-' + panel.id" :class="{ draggable: isActive(panel) }">
106+
<img v-if="panel.iconUrl" alt="" :src="panel.iconUrl">
107+
<span v-else :class="panel.iconClass" aria-hidden="true" />
108+
{{ panel.title }}
109+
</label>
110+
</li>
111+
</template>
103112
</Draggable>
104-
105113
<a v-if="isAdmin && appStoreEnabled" :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the App Store') }}</a>
106114

107115
<div v-if="statuses.weather && isStatusActive('weather')">
@@ -125,7 +133,6 @@ import { getCurrentUser } from '@nextcloud/auth'
125133
import axios from '@nextcloud/axios'
126134
import { loadState } from '@nextcloud/initial-state'
127135
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
128-
import Vue from 'vue'
129136
import Draggable from 'vuedraggable'
130137
import NcButton from '@nextcloud/vue/components/NcButton'
131138
import NcModal from '@nextcloud/vue/components/NcModal'
@@ -176,15 +183,17 @@ export default {
176183
timer: new Date(),
177184
registeredStatus: [],
178185
callbacks: {},
179-
callbacksStatus: {},
180186
allCallbacksStatus: {},
187+
statusElements: {},
181188
statusInfo,
182189
enabledStatuses: loadState('dashboard', 'statuses'),
183190
panels,
184191
firstRun,
185192
displayName: getCurrentUser()?.displayName,
186193
uid: getCurrentUser()?.uid,
187194
layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]),
195+
modalPanelList: [],
196+
isModalDragging: false,
188197
modal: false,
189198
appStoreUrl: generateUrl('/settings/apps/dashboard'),
190199
appStoreEnabled: loadState('dashboard', 'appStoreEnabled', true),
@@ -263,39 +272,19 @@ export default {
263272
return Object.keys(this.allCallbacksStatus).slice().sort(this.sortStatuses)
264273
},
265274
266-
sortedPanels() {
267-
return Object.values(this.panels).sort((a, b) => {
268-
const indexA = this.layout.indexOf(a.id)
269-
const indexB = this.layout.indexOf(b.id)
270-
if (indexA === -1 || indexB === -1) {
271-
return indexB - indexA || a.id - b.id
272-
}
273-
return indexA - indexB || a.id - b.id
274-
})
275-
},
276-
277275
sortedRegisteredStatus() {
278276
return this.registeredStatus.slice().sort(this.sortStatuses)
279277
},
280278
},
281279
282280
watch: {
283281
callbacks() {
284-
this.rerenderPanels()
282+
this.$nextTick(() => this.rerenderPanels())
285283
},
286284
287-
callbacksStatus() {
288-
for (const app in this.callbacksStatus) {
289-
const element = this.$refs['status-' + app]
290-
if (this.statuses[app] && this.statuses[app].mounted) {
291-
continue
292-
}
293-
if (element) {
294-
this.callbacksStatus[app](element[0])
295-
Vue.set(this.statuses, app, { mounted: true })
296-
} else {
297-
logger.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
298-
}
285+
layout() {
286+
if (this.modal && !this.isModalDragging) {
287+
this.modalPanelList = this.getSortedPanelObjects()
299288
}
300289
},
301290
},
@@ -320,6 +309,8 @@ export default {
320309
}, widget.reload_interval * 1000)
321310
}
322311
}
312+
313+
this.$nextTick(() => this.rerenderPanels())
323314
},
324315
325316
mounted() {
@@ -335,7 +326,7 @@ export default {
335326
}
336327
},
337328
338-
destroyed() {
329+
unmounted() {
339330
window.removeEventListener('scroll', this.handleScroll)
340331
},
341332
@@ -347,18 +338,40 @@ export default {
347338
* @param {Function} callback The callback function to register a panel which gets the DOM element passed as parameter
348339
*/
349340
register(app, callback) {
350-
Vue.set(this.callbacks, app, callback)
341+
this.callbacks[app] = callback
351342
},
352343
353344
registerStatus(app, callback) {
354345
// always save callbacks in case user enables the status later
355-
Vue.set(this.allCallbacksStatus, app, callback)
346+
this.allCallbacksStatus[app] = callback
356347
// register only if status is enabled or missing from config
357348
if (this.isStatusActive(app)) {
358-
this.registeredStatus.push(app)
359-
this.$nextTick(() => {
360-
Vue.set(this.callbacksStatus, app, callback)
361-
})
349+
if (!this.registeredStatus.includes(app)) {
350+
this.registeredStatus.push(app)
351+
}
352+
this.$nextTick(() => this.mountStatus(app))
353+
}
354+
},
355+
356+
setStatusElement(app, element) {
357+
if (element) {
358+
this.statusElements[app] = element
359+
this.mountStatus(app)
360+
} else {
361+
delete this.statusElements[app]
362+
}
363+
},
364+
365+
mountStatus(app) {
366+
if (this.statuses[app]?.mounted) {
367+
return
368+
}
369+
370+
const element = this.statusElements[app]
371+
const callback = this.allCallbacksStatus[app]
372+
if (element && callback) {
373+
callback(element)
374+
this.statuses[app] = { mounted: true }
362375
}
363376
},
364377
@@ -377,10 +390,12 @@ export default {
377390
continue
378391
}
379392
if (element) {
380-
this.callbacks[app](element[0], {
393+
// In Vue 3, refs in v-for are arrays
394+
const el = Array.isArray(element) ? element[0] : element
395+
this.callbacks[app](el, {
381396
widget: this.panels[app],
382397
})
383-
Vue.set(this.panels[app], 'mounted', true)
398+
this.panels[app].mounted = true
384399
} else {
385400
logger.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
386401
}
@@ -402,12 +417,40 @@ export default {
402417
showModal() {
403418
this.modal = true
404419
this.firstRun = false
420+
this.modalPanelList = this.getSortedPanelObjects()
405421
},
406422
407423
closeModal() {
408424
this.modal = false
409425
},
410426
427+
getSortedPanelObjects() {
428+
return Object.values(this.panels).sort((a, b) => {
429+
const indexA = this.layout.indexOf(a.id)
430+
const indexB = this.layout.indexOf(b.id)
431+
if (indexA === -1 || indexB === -1) {
432+
return indexB - indexA || a.id - b.id
433+
}
434+
return indexA - indexB || a.id - b.id
435+
})
436+
},
437+
438+
onModalPanelListUpdate(newList) {
439+
this.modalPanelList = newList
440+
},
441+
442+
onModalDragStart() {
443+
this.isModalDragging = true
444+
},
445+
446+
onModalDragEnd() {
447+
this.isModalDragging = false
448+
this.layout = this.modalPanelList
449+
.filter((panel) => this.layout.includes(panel.id))
450+
.map((panel) => panel.id)
451+
this.saveLayout()
452+
},
453+
411454
updateCheckbox(panel, currentValue) {
412455
const index = this.layout.indexOf(panel.id)
413456
if (!currentValue && index > -1) {
@@ -418,7 +461,7 @@ export default {
418461
this.fetchApiWidgetItems([panel.id], true)
419462
}
420463
}
421-
Vue.set(this.panels[panel.id], 'mounted', false)
464+
this.panels[panel.id].mounted = false
422465
this.saveLayout()
423466
this.$nextTick(() => this.rerenderPanels())
424467
},
@@ -457,10 +500,7 @@ export default {
457500
const j = this.registeredStatus.findIndex((s) => s === app)
458501
if (j !== -1) {
459502
this.registeredStatus.splice(j, 1)
460-
Vue.set(this.statuses, app, { mounted: false })
461-
this.$nextTick(() => {
462-
Vue.delete(this.callbacksStatus, app)
463-
})
503+
this.statuses[app] = { mounted: false }
464504
}
465505
this.saveStatuses()
466506
},
@@ -803,4 +843,8 @@ html, body {
803843
/* Scrollbar sits on the background image — use plain-text color for contrast */
804844
scrollbar-color: var(--color-background-plain-text) transparent;
805845
}
846+
847+
#app-content-vue {
848+
width: 100%;
849+
}
806850
</style>

0 commit comments

Comments
 (0)