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 >
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" >
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')" >
124132import { getCurrentUser } from ' @nextcloud/auth'
125133import axios from ' @nextcloud/axios'
126134import { loadState } from ' @nextcloud/initial-state'
135+ import { t } from ' @nextcloud/l10n'
127136import { generateOcsUrl , generateUrl } from ' @nextcloud/router'
128- import Vue from ' vue'
129- import Draggable from ' vuedraggable'
137+ // import this directly so Vite builds it and we don't end up using the UMD bundle which
138+ // tries to run "new Function(...)" (for no good reason) which is blocked by our CSP
139+ import Draggable from ' vuedraggable/src/vuedraggable.js'
130140import NcButton from ' @nextcloud/vue/components/NcButton'
131141import NcModal from ' @nextcloud/vue/components/NcModal'
132142import NcUserStatusIcon from ' @nextcloud/vue/components/NcUserStatusIcon'
@@ -166,6 +176,7 @@ export default {
166176
167177 setup () {
168178 return {
179+ t,
169180 productName: window .OC .theme .productName ,
170181 }
171182 },
@@ -176,15 +187,17 @@ export default {
176187 timer: new Date (),
177188 registeredStatus: [],
178189 callbacks: {},
179- callbacksStatus: {},
180190 allCallbacksStatus: {},
191+ statusElements: {},
181192 statusInfo,
182193 enabledStatuses: loadState (' dashboard' , ' statuses' ),
183194 panels,
184195 firstRun,
185196 displayName: getCurrentUser ()? .displayName ,
186197 uid: getCurrentUser ()? .uid ,
187198 layout: loadState (' dashboard' , ' layout' ).filter ((panelId ) => panels[panelId]),
199+ modalPanelList: [],
200+ isModalDragging: false ,
188201 modal: false ,
189202 appStoreUrl: generateUrl (' /settings/apps/dashboard' ),
190203 appStoreEnabled: loadState (' dashboard' , ' appStoreEnabled' , true ),
@@ -263,39 +276,19 @@ export default {
263276 return Object .keys (this .allCallbacksStatus ).slice ().sort (this .sortStatuses )
264277 },
265278
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-
277279 sortedRegisteredStatus () {
278280 return this .registeredStatus .slice ().sort (this .sortStatuses )
279281 },
280282 },
281283
282284 watch: {
283285 callbacks () {
284- this .rerenderPanels ()
286+ this .$nextTick (() => this . rerenderPanels () )
285287 },
286288
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- }
289+ layout () {
290+ if (this .modal && ! this .isModalDragging ) {
291+ this .modalPanelList = this .getSortedPanelObjects ()
299292 }
300293 },
301294 },
@@ -320,6 +313,8 @@ export default {
320313 }, widget .reload_interval * 1000 )
321314 }
322315 }
316+
317+ this .$nextTick (() => this .rerenderPanels ())
323318 },
324319
325320 mounted () {
@@ -335,7 +330,7 @@ export default {
335330 }
336331 },
337332
338- destroyed () {
333+ unmounted () {
339334 window .removeEventListener (' scroll' , this .handleScroll )
340335 },
341336
@@ -347,18 +342,40 @@ export default {
347342 * @param {Function} callback The callback function to register a panel which gets the DOM element passed as parameter
348343 */
349344 register (app , callback ) {
350- Vue . set ( this .callbacks , app, callback)
345+ this .callbacks [ app] = callback
351346 },
352347
353348 registerStatus (app , callback ) {
354349 // always save callbacks in case user enables the status later
355- Vue . set ( this .allCallbacksStatus , app, callback)
350+ this .allCallbacksStatus [ app] = callback
356351 // register only if status is enabled or missing from config
357352 if (this .isStatusActive (app)) {
358- this .registeredStatus .push (app)
359- this .$nextTick (() => {
360- Vue .set (this .callbacksStatus , app, callback)
361- })
353+ if (! this .registeredStatus .includes (app)) {
354+ this .registeredStatus .push (app)
355+ }
356+ this .$nextTick (() => this .mountStatus (app))
357+ }
358+ },
359+
360+ setStatusElement (app , element ) {
361+ if (element) {
362+ this .statusElements [app] = element
363+ this .mountStatus (app)
364+ } else {
365+ delete this .statusElements [app]
366+ }
367+ },
368+
369+ mountStatus (app ) {
370+ if (this .statuses [app]? .mounted ) {
371+ return
372+ }
373+
374+ const element = this .statusElements [app]
375+ const callback = this .allCallbacksStatus [app]
376+ if (element && callback) {
377+ callback (element)
378+ this .statuses [app] = { mounted: true }
362379 }
363380 },
364381
@@ -377,10 +394,12 @@ export default {
377394 continue
378395 }
379396 if (element) {
380- this .callbacks [app](element[0 ], {
397+ // In Vue 3, refs in v-for are arrays
398+ const el = Array .isArray (element) ? element[0 ] : element
399+ this .callbacks [app](el, {
381400 widget: this .panels [app],
382401 })
383- Vue . set ( this .panels [app], ' mounted' , true )
402+ this .panels [app]. mounted = true
384403 } else {
385404 logger .error (' Failed to register panel in the frontend as no backend data was provided for ' + app)
386405 }
@@ -402,12 +421,40 @@ export default {
402421 showModal () {
403422 this .modal = true
404423 this .firstRun = false
424+ this .modalPanelList = this .getSortedPanelObjects ()
405425 },
406426
407427 closeModal () {
408428 this .modal = false
409429 },
410430
431+ getSortedPanelObjects () {
432+ return Object .values (this .panels ).sort ((a , b ) => {
433+ const indexA = this .layout .indexOf (a .id )
434+ const indexB = this .layout .indexOf (b .id )
435+ if (indexA === - 1 || indexB === - 1 ) {
436+ return indexB - indexA || a .id - b .id
437+ }
438+ return indexA - indexB || a .id - b .id
439+ })
440+ },
441+
442+ onModalPanelListUpdate (newList ) {
443+ this .modalPanelList = newList
444+ },
445+
446+ onModalDragStart () {
447+ this .isModalDragging = true
448+ },
449+
450+ onModalDragEnd () {
451+ this .isModalDragging = false
452+ this .layout = this .modalPanelList
453+ .filter ((panel ) => this .layout .includes (panel .id ))
454+ .map ((panel ) => panel .id )
455+ this .saveLayout ()
456+ },
457+
411458 updateCheckbox (panel , currentValue ) {
412459 const index = this .layout .indexOf (panel .id )
413460 if (! currentValue && index > - 1 ) {
@@ -418,7 +465,7 @@ export default {
418465 this .fetchApiWidgetItems ([panel .id ], true )
419466 }
420467 }
421- Vue . set ( this .panels [panel .id ], ' mounted' , false )
468+ this .panels [panel .id ]. mounted = false
422469 this .saveLayout ()
423470 this .$nextTick (() => this .rerenderPanels ())
424471 },
@@ -457,10 +504,7 @@ export default {
457504 const j = this .registeredStatus .findIndex ((s ) => s === app)
458505 if (j !== - 1 ) {
459506 this .registeredStatus .splice (j, 1 )
460- Vue .set (this .statuses , app, { mounted: false })
461- this .$nextTick (() => {
462- Vue .delete (this .callbacksStatus , app)
463- })
507+ this .statuses [app] = { mounted: false }
464508 }
465509 this .saveStatuses ()
466510 },
@@ -517,6 +561,11 @@ export default {
517561< / script>
518562
519563< style lang= " scss" scoped>
564+ : global (#content .app - dashboard ) {
565+ margin- inline: 0 ;
566+ width: 100 % ;
567+ }
568+
520569#app- dashboard {
521570 width: 100 % ;
522571 min- height: 100 % ;
@@ -803,4 +852,8 @@ html, body {
803852 /* Scrollbar sits on the background image — use plain-text color for contrast */
804853 scrollbar- color: var (-- color- background- plain- text) transparent;
805854}
855+
856+ #app- content- vue {
857+ width: 100 % ;
858+ }
806859< / style>
0 commit comments