]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(dashboard): implement widget item api v2
authorRichard Steinmetz <richard@steinmetz.cloud>
Thu, 17 Aug 2023 13:09:30 +0000 (15:09 +0200)
committerJoas Schilling <coding@schilljs.com>
Tue, 22 Aug 2023 08:38:46 +0000 (10:38 +0200)
This API enables the dashboard to render all widgets from the API data
alone without having apps to provide their own bundles. This saves a lot
of traffic and execution time as a lot less javascript has to be parsed
on the frontend.

Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
16 files changed:
apps/dashboard/appinfo/routes.php
apps/dashboard/lib/Controller/DashboardApiController.php
apps/dashboard/openapi.json [deleted file]
apps/dashboard/src/DashboardApp.vue
apps/dashboard/src/components/ApiDashboardWidget.vue [new file with mode: 0644]
apps/user_status/lib/Dashboard/UserStatusWidget.php
apps/user_status/src/dashboard.js [deleted file]
apps/user_status/src/views/Dashboard.vue [deleted file]
apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/public/Dashboard/IAPIWidgetV2.php [new file with mode: 0644]
lib/public/Dashboard/IReloadableWidget.php [new file with mode: 0644]
lib/public/Dashboard/Model/WidgetItem.php
lib/public/Dashboard/Model/WidgetItems.php [new file with mode: 0644]
webpack.modules.js

index c6891837384c66fbd08e8c1a9f79be241edd7445..e872c47084b32b0d3784732a0064158df2e244ba 100644 (file)
@@ -7,6 +7,7 @@ declare(strict_types=1);
  *
  * @author Julien Veyssier <eneiluj@posteo.net>
  * @author Julius Härtl <jus@bitgrid.net>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -33,5 +34,6 @@ return [
        'ocs' => [
                ['name' => 'dashboardApi#getWidgets', 'url' => '/api/v1/widgets', 'verb' => 'GET'],
                ['name' => 'dashboardApi#getWidgetItems', 'url' => '/api/v1/widget-items', 'verb' => 'GET'],
+               ['name' => 'dashboardApi#getWidgetItemsV2', 'url' => '/api/v2/widget-items', 'verb' => 'GET'],
        ]
 ];
index 1062cf1bdba021df645570665b774c32ec203c53..0b2b05e0df6577579cdda5f48f0d1cf97617cb08 100644 (file)
@@ -6,6 +6,7 @@ declare(strict_types=1);
  * @copyright Copyright (c) 2021 Julien Veyssier <eneiluj@posteo.net>
  *
  * @author Julien Veyssier <eneiluj@posteo.net>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -32,6 +33,7 @@ use OCP\Dashboard\IButtonWidget;
 use OCP\Dashboard\IIconWidget;
 use OCP\Dashboard\IOptionWidget;
 use OCP\Dashboard\IManager;
+use OCP\Dashboard\IReloadableWidget;
 use OCP\Dashboard\IWidget;
 use OCP\Dashboard\Model\WidgetButton;
 use OCP\Dashboard\Model\WidgetOptions;
@@ -39,7 +41,9 @@ use OCP\IConfig;
 use OCP\IRequest;
 
 use OCP\Dashboard\IAPIWidget;
+use OCP\Dashboard\IAPIWidgetV2;
 use OCP\Dashboard\Model\WidgetItem;
+use OCP\Dashboard\Model\WidgetItems;
 
 class DashboardApiController extends OCSController {
 
@@ -68,6 +72,24 @@ class DashboardApiController extends OCSController {
         * Example request with Curl:
         * curl -u user:passwd http://my.nc/ocs/v2.php/apps/dashboard/api/v1/widget-items -H Content-Type:application/json -X GET -d '{"sinceIds":{"github_notifications":"2021-03-22T15:01:10Z"}}'
         *
+        * @param string[] $widgetIds Limit widgets to given ids
+        * @return IWidget[]
+        */
+       private function getShownWidgets(array $widgetIds): array {
+               if (empty($widgetIds)) {
+                       $systemDefault = $this->config->getAppValue('dashboard', 'layout', 'recommendations,spreed,mail,calendar');
+                       $widgetIds = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', $systemDefault));
+               }
+
+               return array_filter(
+                       $this->dashboardManager->getWidgets(),
+                       static function (IWidget $widget) use ($widgetIds) {
+                               return in_array($widget->getId(), $widgetIds);
+                       },
+               );
+       }
+
+       /**
         * @param array $sinceIds Array indexed by widget Ids, contains date/id from which we want the new items
         * @param int $limit Limit number of result items per widget
         * @param string[] $widgets Limit results to specific widgets
@@ -76,18 +98,11 @@ class DashboardApiController extends OCSController {
         * @NoCSRFRequired
         */
        public function getWidgetItems(array $sinceIds = [], int $limit = 7, array $widgets = []): DataResponse {
-               $showWidgets = $widgets;
                $items = [];
-
-               if (empty($showWidgets)) {
-                       $systemDefault = $this->config->getAppValue('dashboard', 'layout', 'recommendations,spreed,mail,calendar');
-                       $showWidgets = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', $systemDefault));
-               }
-
-               $widgets = $this->dashboardManager->getWidgets();
+               $widgets = $this->getShownWidgets($widgets);
                foreach ($widgets as $widget) {
-                       if ($widget instanceof IAPIWidget && in_array($widget->getId(), $showWidgets)) {
-                               $items[$widget->getId()] = array_map(function (WidgetItem $item) {
+                       if ($widget instanceof IAPIWidget) {
+                               $items[$widget->getId()] = array_map(static function (WidgetItem $item) {
                                        return $item->jsonSerialize();
                                }, $widget->getItems($this->userId, $sinceIds[$widget->getId()] ?? null, $limit));
                        }
@@ -102,6 +117,33 @@ class DashboardApiController extends OCSController {
         *
         * @NoAdminRequired
         * @NoCSRFRequired
+        *
+        * Get the items for the widgets
+        *
+        * @param array<string, string> $sinceIds Array indexed by widget Ids, contains date/id from which we want the new items
+        * @param int $limit Limit number of result items per widget
+        * @param string[] $widgets Limit results to specific widgets
+        * @return DataResponse<Http::STATUS_OK, array<string, DashboardWidgetItems>, array{}>
+        */
+       public function getWidgetItemsV2(array $sinceIds = [], int $limit = 7, array $widgets = []): DataResponse {
+               $items = [];
+               $widgets = $this->getShownWidgets($widgets);
+               foreach ($widgets as $widget) {
+                       if ($widget instanceof IAPIWidgetV2) {
+                               $items[$widget->getId()] = $widget
+                                       ->getItemsV2($this->userId, $sinceIds[$widget->getId()] ?? null, $limit)
+                                       ->jsonSerialize();
+                       }
+               }
+
+               return new DataResponse($items);
+       }
+
+       /**
+        * Get the widgets
+        *
+        * @NoAdminRequired
+        * @NoCSRFRequired
         */
        public function getWidgets(): DataResponse {
                $widgets = $this->dashboardManager->getWidgets();
@@ -116,6 +158,8 @@ class DashboardApiController extends OCSController {
                                'icon_url' => ($widget instanceof IIconWidget) ? $widget->getIconUrl() : '',
                                'widget_url' => $widget->getUrl(),
                                'item_icons_round' => $options->withRoundItemIcons(),
+                               'item_api_versions' => [],
+                               'reload_interval' => 0,
                        ];
                        if ($widget instanceof IButtonWidget) {
                                $data += [
@@ -128,6 +172,15 @@ class DashboardApiController extends OCSController {
                                        }, $widget->getWidgetButtons($this->userId)),
                                ];
                        }
+                       if ($widget instanceof IReloadableWidget) {
+                               $data['reload_interval'] = $widget->getReloadInterval();
+                       }
+                       if ($widget instanceof IAPIWidget) {
+                               $data['item_api_versions'][] = 1;
+                       }
+                       if ($widget instanceof IAPIWidgetV2) {
+                               $data['item_api_versions'][] = 2;
+                       }
                        return $data;
                }, $widgets);
 
diff --git a/apps/dashboard/openapi.json b/apps/dashboard/openapi.json
deleted file mode 100644 (file)
index cf706f1..0000000
+++ /dev/null
@@ -1,293 +0,0 @@
-{
-    "openapi": "3.0.3",
-    "info": {
-        "title": "dashboard",
-        "version": "0.0.1",
-        "description": "Dashboard app",
-        "license": {
-            "name": "agpl"
-        }
-    },
-    "components": {
-        "securitySchemes": {
-            "basic_auth": {
-                "type": "http",
-                "scheme": "basic"
-            },
-            "bearer_auth": {
-                "type": "http",
-                "scheme": "bearer"
-            }
-        },
-        "schemas": {
-            "OCSMeta": {
-                "type": "object",
-                "required": [
-                    "status",
-                    "statuscode"
-                ],
-                "properties": {
-                    "status": {
-                        "type": "string"
-                    },
-                    "statuscode": {
-                        "type": "integer"
-                    },
-                    "message": {
-                        "type": "string"
-                    },
-                    "totalitems": {
-                        "type": "string"
-                    },
-                    "itemsperpage": {
-                        "type": "string"
-                    }
-                }
-            },
-            "Widget": {
-                "type": "object",
-                "required": [
-                    "id",
-                    "title",
-                    "order",
-                    "icon_class",
-                    "icon_url",
-                    "widget_url",
-                    "item_icons_round"
-                ],
-                "properties": {
-                    "id": {
-                        "type": "string"
-                    },
-                    "title": {
-                        "type": "string"
-                    },
-                    "order": {
-                        "type": "integer",
-                        "format": "int64"
-                    },
-                    "icon_class": {
-                        "type": "string"
-                    },
-                    "icon_url": {
-                        "type": "string"
-                    },
-                    "widget_url": {
-                        "type": "string",
-                        "nullable": true
-                    },
-                    "item_icons_round": {
-                        "type": "boolean"
-                    },
-                    "buttons": {
-                        "type": "array",
-                        "items": {
-                            "type": "object",
-                            "required": [
-                                "type",
-                                "text",
-                                "link"
-                            ],
-                            "properties": {
-                                "type": {
-                                    "type": "string"
-                                },
-                                "text": {
-                                    "type": "string"
-                                },
-                                "link": {
-                                    "type": "string"
-                                }
-                            }
-                        }
-                    }
-                }
-            },
-            "WidgetItem": {
-                "type": "object",
-                "required": [
-                    "subtitle",
-                    "title",
-                    "link",
-                    "iconUrl",
-                    "sinceId"
-                ],
-                "properties": {
-                    "subtitle": {
-                        "type": "string"
-                    },
-                    "title": {
-                        "type": "string"
-                    },
-                    "link": {
-                        "type": "string"
-                    },
-                    "iconUrl": {
-                        "type": "string"
-                    },
-                    "sinceId": {
-                        "type": "string"
-                    }
-                }
-            }
-        }
-    },
-    "paths": {
-        "/ocs/v2.php/apps/dashboard/api/v1/widgets": {
-            "get": {
-                "operationId": "dashboard_api-get-widgets",
-                "summary": "Get the widgets",
-                "tags": [
-                    "dashboard_api"
-                ],
-                "security": [
-                    {
-                        "bearer_auth": []
-                    },
-                    {
-                        "basic_auth": []
-                    }
-                ],
-                "parameters": [
-                    {
-                        "name": "OCS-APIRequest",
-                        "in": "header",
-                        "required": true,
-                        "schema": {
-                            "type": "string",
-                            "default": "true"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "",
-                        "content": {
-                            "application/json": {
-                                "schema": {
-                                    "type": "object",
-                                    "required": [
-                                        "ocs"
-                                    ],
-                                    "properties": {
-                                        "ocs": {
-                                            "type": "object",
-                                            "required": [
-                                                "meta",
-                                                "data"
-                                            ],
-                                            "properties": {
-                                                "meta": {
-                                                    "$ref": "#/components/schemas/OCSMeta"
-                                                },
-                                                "data": {
-                                                    "type": "array",
-                                                    "items": {
-                                                        "$ref": "#/components/schemas/Widget"
-                                                    }
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        },
-        "/ocs/v2.php/apps/dashboard/api/v1/widget-items": {
-            "get": {
-                "operationId": "dashboard_api-get-widget-items",
-                "summary": "Get the items for the widgets",
-                "tags": [
-                    "dashboard_api"
-                ],
-                "security": [
-                    {
-                        "bearer_auth": []
-                    },
-                    {
-                        "basic_auth": []
-                    }
-                ],
-                "parameters": [
-                    {
-                        "name": "sinceIds",
-                        "in": "query",
-                        "description": "Array indexed by widget Ids, contains date/id from which we want the new items",
-                        "schema": {
-                            "type": "string"
-                        }
-                    },
-                    {
-                        "name": "limit",
-                        "in": "query",
-                        "description": "Limit number of result items per widget",
-                        "schema": {
-                            "type": "integer",
-                            "format": "int64",
-                            "default": 7
-                        }
-                    },
-                    {
-                        "name": "widgets",
-                        "in": "query",
-                        "description": "Limit results to specific widgets",
-                        "schema": {
-                            "type": "string"
-                        }
-                    },
-                    {
-                        "name": "OCS-APIRequest",
-                        "in": "header",
-                        "required": true,
-                        "schema": {
-                            "type": "string",
-                            "default": "true"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "",
-                        "content": {
-                            "application/json": {
-                                "schema": {
-                                    "type": "object",
-                                    "required": [
-                                        "ocs"
-                                    ],
-                                    "properties": {
-                                        "ocs": {
-                                            "type": "object",
-                                            "required": [
-                                                "meta",
-                                                "data"
-                                            ],
-                                            "properties": {
-                                                "meta": {
-                                                    "$ref": "#/components/schemas/OCSMeta"
-                                                },
-                                                "data": {
-                                                    "type": "object",
-                                                    "additionalProperties": {
-                                                        "type": "array",
-                                                        "items": {
-                                                            "$ref": "#/components/schemas/WidgetItem"
-                                                        }
-                                                    }
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    },
-    "tags": []
-}
\ No newline at end of file
index f0c1dfbd734bd8a799bedd3e4408448ba463982b..ffdf368cb2cc30839c1e952629fb4b86ec67f858 100644 (file)
                        v-bind="{swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3}"
                        handle=".panel--header"
                        @end="saveLayout">
-                       <div v-for="panelId in layout" :key="panels[panelId].id" class="panel">
-                               <div class="panel--header">
-                                       <h2>
-                                               <div aria-labelledby="panel--header--icon--description"
-                                                       aria-hidden="true"
-                                                       :class="panels[panelId].iconClass"
-                                                       role="img" />
-                                               {{ panels[panelId].title }}
-                                       </h2>
-                                       <span id="panel--header--icon--description" class="hidden-visually"> {{ t('dashboard', '"{title} icon"', { title: panels[panelId].title }) }} </span>
+                       <template v-for="panelId in layout">
+                               <div v-if="isApiWidgetV2(panels[panelId].id)"
+                                       :key="`${panels[panelId].id}-v2`"
+                                       class="panel">
+                                       <div class="panel--header">
+                                               <h2>
+                                                       <div aria-labelledby="panel--header--icon--description"
+                                                               aria-hidden="true"
+                                                               :class="apiWidgets[panels[panelId].id].icon_class"
+                                                               role="img" />
+                                                       {{ apiWidgets[panels[panelId].id].title }}
+                                               </h2>
+                                               <span id="panel--header--icon--description" class="hidden-visually">
+                                                       {{ t('dashboard', '"{title} icon"', { title: apiWidgets[panels[panelId].id].title }) }}
+                                               </span>
+                                       </div>
+                                       <div class="panel--content">
+                                               <ApiDashboardWidget :widget="apiWidgets[panels[panelId].id]"
+                                                       :data="apiWidgetItems[panels[panelId].id]"
+                                                       :loading="loadingItems" />
+                                       </div>
                                </div>
-                               <div class="panel--content" :class="{ loading: !panels[panelId].mounted }">
-                                       <div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
+                               <div v-else :key="panels[panelId].id" class="panel">
+                                       <div class="panel--header">
+                                               <h2>
+                                                       <div aria-labelledby="panel--header--icon--description"
+                                                               aria-hidden="true"
+                                                               :class="panels[panelId].iconClass"
+                                                               role="img" />
+                                                       {{ panels[panelId].title }}
+                                               </h2>
+                                               <span id="panel--header--icon--description" class="hidden-visually"> {{ t('dashboard', '"{title} icon"', { title: panels[panelId].title }) }} </span>
+                                       </div>
+                                       <div class="panel--content" :class="{ loading: !panels[panelId].mounted }">
+                                               <div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
+                                       </div>
                                </div>
-                       </div>
+                       </template>
                </Draggable>
 
                <div class="footer">
 </template>
 
 <script>
-import { generateUrl } from '@nextcloud/router'
+import { generateUrl, generateOcsUrl } from '@nextcloud/router'
 import { getCurrentUser } from '@nextcloud/auth'
 import { loadState } from '@nextcloud/initial-state'
 import axios from '@nextcloud/axios'
@@ -105,6 +128,7 @@ import Pencil from 'vue-material-design-icons/Pencil.vue'
 import Vue from 'vue'
 
 import isMobile from './mixins/isMobile.js'
+import ApiDashboardWidget from './components/ApiDashboardWidget.vue'
 
 const panels = loadState('dashboard', 'panels')
 const firstRun = loadState('dashboard', 'firstRun')
@@ -123,6 +147,7 @@ const statusInfo = {
 export default {
        name: 'DashboardApp',
        components: {
+               ApiDashboardWidget,
                NcButton,
                Draggable,
                NcModal,
@@ -150,6 +175,9 @@ export default {
                        modal: false,
                        appStoreUrl: generateUrl('/settings/apps/dashboard'),
                        statuses: {},
+                       apiWidgets: [],
+                       apiWidgetItems: {},
+                       loadingItems: true,
                }
        },
        computed: {
@@ -239,6 +267,23 @@ export default {
                },
        },
 
+       async created() {
+               await this.fetchApiWidgets()
+
+               const apiWidgetIdsToFetch = Object
+                       .values(this.apiWidgets)
+                       .filter(widget => this.isApiWidgetV2(widget.id))
+                       .map(widget => widget.id)
+               await Promise.all(apiWidgetIdsToFetch.map(id => this.fetchApiWidgetItems([id], true)))
+
+               for (const widget of Object.values(this.apiWidgets)) {
+                       if (widget.reload_interval > 0) {
+                               setInterval(async () => {
+                                       await this.fetchApiWidgetItems([widget.id], true)
+                               }, widget.reload_interval * 1000)
+                       }
+               }
+       },
        mounted() {
                this.updateSkipLink()
                window.addEventListener('scroll', this.handleScroll)
@@ -278,6 +323,11 @@ export default {
                },
                rerenderPanels() {
                        for (const app in this.callbacks) {
+                               // TODO: Properly rerender v2 widgets
+                               if (this.isApiWidgetV2(this.panels[app].id)) {
+                                       continue
+                               }
+
                                const element = this.$refs[app]
                                if (this.layout.indexOf(app) === -1) {
                                        continue
@@ -374,6 +424,33 @@ export default {
                                document.body.classList.remove('dashboard--scrolled')
                        }
                },
+               async fetchApiWidgets() {
+                       const response = await axios.get(generateOcsUrl('/apps/dashboard/api/v1/widgets'))
+                       this.apiWidgets = response.data.ocs.data
+               },
+               async fetchApiWidgetItems(widgetIds, merge = false) {
+                       try {
+                               const url = generateOcsUrl('/apps/dashboard/api/v2/widget-items')
+                               const params = new URLSearchParams(widgetIds.map(id => ['widgets[]', id]))
+                               const response = await axios.get(`${url}?${params.toString()}`)
+                               const widgetItems = response.data.ocs.data
+                               if (merge) {
+                                       this.apiWidgetItems = Object.assign({}, this.apiWidgetItems, widgetItems)
+                               } else {
+                                       this.apiWidgetItems = widgetItems
+                               }
+                       } finally {
+                               this.loadingItems = false
+                       }
+               },
+               isApiWidgetV2(id) {
+                       for (const widget of Object.values(this.apiWidgets)) {
+                               if (widget.id === id && widget.item_api_versions.includes(2)) {
+                                       return true
+                               }
+                       }
+                       return false
+               },
        },
 }
 </script>
@@ -470,6 +547,7 @@ export default {
                                margin-right: 16px;
                                background-position: center;
                                float: left;
+                               margin-top: -6px;
                        }
                }
        }
diff --git a/apps/dashboard/src/components/ApiDashboardWidget.vue b/apps/dashboard/src/components/ApiDashboardWidget.vue
new file mode 100644 (file)
index 0000000..daa97fc
--- /dev/null
@@ -0,0 +1,140 @@
+<!--
+  - @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+  -
+  - @author Richard Steinmetz <richard@steinmetz.cloud>
+  -
+  - @license AGPL-3.0-or-later
+  -
+  - This program is free software: you can redistribute it and/or modify
+  - it under the terms of the GNU General Public License as published by
+  - the Free Software Foundation, either version 3 of the License, or
+  - (at your option) any later version.
+  -
+  - This program is distributed in the hope that it will be useful,
+  - but WITHOUT ANY WARRANTY; without even the implied warranty of
+  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  - GNU General Public License for more details.
+  -
+  - You should have received a copy of the GNU General Public License
+  - along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -
+  -->
+
+<template>
+       <NcDashboardWidget :items="items"
+               :show-more-label="showMoreLabel"
+               :show-more-url="showMoreUrl"
+               :loading="loading"
+               :show-items-and-empty-content="!!halfEmptyContentMessage"
+               :half-empty-content-message="halfEmptyContentMessage">
+               <template #default="{ item }">
+                       <NcDashboardWidgetItem :target-url="item.link"
+                               :overlay-icon-url="item.overlayIconUrl ? item.overlayIconUrl : ''"
+                               :main-text="item.title"
+                               :sub-text="item.subtitle">
+                               <template #avatar>
+                                       <template v-if="item.iconUrl">
+                                               <NcAvatar :size="44" :url="item.iconUrl" />
+                                       </template>
+                               </template>
+                       </NcDashboardWidgetItem>
+               </template>
+               <template #empty-content>
+                       <NcEmptyContent v-if="items.length === 0"
+                               :description="emptyContentMessage">
+                               <template #icon>
+                                       <CheckIcon v-if="emptyContentMessage" :size="65" />
+                               </template>
+                               <template #action>
+                                       <NcButton v-if="setupButton" :href="setupButton.link">
+                                               {{ setupButton.text }}
+                                       </NcButton>
+                               </template>
+                       </NcEmptyContent>
+               </template>
+       </NcDashboardWidget>
+</template>
+
+<script>
+import {
+       NcAvatar,
+       NcDashboardWidget,
+       NcDashboardWidgetItem,
+       NcEmptyContent,
+       NcButton,
+} from '@nextcloud/vue'
+import CheckIcon from 'vue-material-design-icons/Check.vue'
+
+export default {
+       name: 'ApiDashboardWidget',
+       components: {
+               NcAvatar,
+               NcDashboardWidget,
+               NcDashboardWidgetItem,
+               NcEmptyContent,
+               NcButton,
+               CheckIcon,
+       },
+       props: {
+               widget: {
+                       type: [Object, undefined],
+                       default: undefined,
+               },
+               data: {
+                       type: [Object, undefined],
+                       default: undefined,
+               },
+               loading: {
+                       type: Boolean,
+                       required: true,
+               },
+       },
+       computed: {
+               /** @return {object[]} */
+               items() {
+                       return this.data?.items ?? []
+               },
+
+               /** @return {string} */
+               emptyContentMessage() {
+                       return this.data?.emptyContentMessage ?? ''
+               },
+
+               /** @return {string} */
+               halfEmptyContentMessage() {
+                       return this.data?.halfEmptyContentMessage ?? ''
+               },
+
+               /** @return {object|undefined} */
+               newButton() {
+                       // TODO: Render new button in the template
+                       // I couldn't find a widget that makes use of the button. Furthermore, there is no convenient
+                       // way to render such a button using the official widget component.
+                       return this.widget?.buttons?.find(button => button.type === 'new')
+               },
+
+               /** @return {object|undefined} */
+               moreButton() {
+                       return this.widget?.buttons?.find(button => button.type === 'more')
+               },
+
+               /** @return {object|undefined} */
+               setupButton() {
+                       return this.widget?.buttons?.find(button => button.type === 'setup')
+               },
+
+               /** @return {string|undefined} */
+               showMoreLabel() {
+                       return this.moreButton?.text
+               },
+
+               /** @return {string|undefined} */
+               showMoreUrl() {
+                       return this.moreButton?.link
+               },
+       },
+}
+</script>
+
+<style lang="scss" scoped>
+</style>
index 2e1de3cd6cf7f93f1b2fa6b63a1aaff201748056..89b9c05f8053db7aa95430d5c92ef91f0b0299c4 100644 (file)
@@ -6,6 +6,7 @@ declare(strict_types=1);
  * @copyright Copyright (c) 2020, Georg Ehrke
  *
  * @author Georg Ehrke <oc.list@georgehrke.com>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -31,9 +32,11 @@ use OCA\UserStatus\Service\StatusService;
 use OCP\AppFramework\Services\IInitialState;
 use OCP\Dashboard\IAPIWidget;
 use OCP\Dashboard\IButtonWidget;
+use OCP\Dashboard\IAPIWidgetV2;
 use OCP\Dashboard\IIconWidget;
 use OCP\Dashboard\IOptionWidget;
 use OCP\Dashboard\Model\WidgetItem;
+use OCP\Dashboard\Model\WidgetItems;
 use OCP\Dashboard\Model\WidgetOptions;
 use OCP\IDateTimeFormatter;
 use OCP\IL10N;
@@ -48,7 +51,7 @@ use OCP\Util;
  *
  * @package OCA\UserStatus
  */
-class UserStatusWidget implements IAPIWidget, IIconWidget, IOptionWidget {
+class UserStatusWidget implements IAPIWidget, IAPIWidgetV2, IIconWidget, IOptionWidget {
        private IL10N $l10n;
        private IDateTimeFormatter $dateTimeFormatter;
        private IURLGenerator $urlGenerator;
@@ -132,17 +135,6 @@ class UserStatusWidget implements IAPIWidget, IIconWidget, IOptionWidget {
         * @inheritDoc
         */
        public function load(): void {
-               Util::addScript(Application::APP_ID, 'dashboard');
-
-               $currentUser = $this->userSession->getUser();
-               if ($currentUser === null) {
-                       $this->initialStateService->provideInitialState('dashboard_data', []);
-                       return;
-               }
-               $currentUserId = $currentUser->getUID();
-
-               $widgetItemsData = $this->getWidgetData($currentUserId);
-               $this->initialStateService->provideInitialState('dashboard_data', $widgetItemsData);
        }
 
        private function getWidgetData(string $userId, ?string $since = null, int $limit = 7): array {
@@ -201,6 +193,17 @@ class UserStatusWidget implements IAPIWidget, IIconWidget, IOptionWidget {
                }, $widgetItemsData);
        }
 
+       /**
+        * @inheritDoc
+        */
+       public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems {
+               $items = $this->getItems($userId, $since, $limit);
+               return new WidgetItems(
+                       $items,
+                       count($items) === 0 ? $this->l10n->t('No recent status changes') : '',
+               );
+       }
+
        public function getWidgetOptions(): WidgetOptions {
                return new WidgetOptions(true);
        }
diff --git a/apps/user_status/src/dashboard.js b/apps/user_status/src/dashboard.js
deleted file mode 100644 (file)
index 4554dcb..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @copyright Copyright (c) 2020 Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import Vue from 'vue'
-import { getRequestToken } from '@nextcloud/auth'
-import { translate, translatePlural } from '@nextcloud/l10n'
-import Dashboard from './views/Dashboard.vue'
-
-// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
-
-Vue.prototype.t = translate
-Vue.prototype.n = translatePlural
-Vue.prototype.OC = OC
-Vue.prototype.OCA = OCA
-
-document.addEventListener('DOMContentLoaded', function() {
-       OCA.Dashboard.register('user_status', (el) => {
-               const View = Vue.extend(Dashboard)
-               new View({
-                       propsData: {},
-               }).$mount(el)
-       })
-
-})
diff --git a/apps/user_status/src/views/Dashboard.vue b/apps/user_status/src/views/Dashboard.vue
deleted file mode 100644 (file)
index 1f9201c..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-<!--
-  - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com>
-  - @author Georg Ehrke <oc.list@georgehrke.com>
-  -
-  - @license GNU AGPL version 3 or any later version
-  -
-  - This program is free software: you can redistribute it and/or modify
-  - it under the terms of the GNU Affero General Public License as
-  - published by the Free Software Foundation, either version 3 of the
-  - License, or (at your option) any later version.
-  -
-  - This program is distributed in the hope that it will be useful,
-  - but WITHOUT ANY WARRANTY; without even the implied warranty of
-  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-  - GNU Affero General Public License for more details.
-  -
-  - You should have received a copy of the GNU Affero General Public License
-  - along with this program. If not, see <http://www.gnu.org/licenses/>.
-  -
-  -->
-
-<template>
-       <NcDashboardWidget id="user-status_panel"
-               :items="items"
-               :loading="loading"
-               :empty-content-message="t('user_status', 'No recent status changes')">
-               <template #default="{ item }">
-                       <NcDashboardWidgetItem :main-text="item.mainText"
-                               :sub-text="item.subText">
-                               <template #avatar>
-                                       <NcAvatar class="item-avatar"
-                                               :size="44"
-                                               :user="item.avatarUsername"
-                                               :display-name="item.mainText"
-                                               :show-user-status="false"
-                                               :show-user-status-compact="false" />
-                               </template>
-                       </NcDashboardWidgetItem>
-               </template>
-               <template #emptyContentIcon>
-                       <div class="icon-user-status-dark" />
-               </template>
-       </NcDashboardWidget>
-</template>
-
-<script>
-import { loadState } from '@nextcloud/initial-state'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcDashboardWidget from '@nextcloud/vue/dist/Components/NcDashboardWidget.js'
-import NcDashboardWidgetItem from '@nextcloud/vue/dist/Components/NcDashboardWidgetItem.js'
-import moment from '@nextcloud/moment'
-
-export default {
-       name: 'Dashboard',
-       components: {
-               NcAvatar,
-               NcDashboardWidget,
-               NcDashboardWidgetItem,
-       },
-       data() {
-               return {
-                       statuses: [],
-                       loading: true,
-               }
-       },
-       computed: {
-               items() {
-                       return this.statuses.map((item) => {
-                               const icon = item.icon || ''
-                               let message = item.message || ''
-                               if (message === '') {
-                                       if (item.status === 'away') {
-                                               message = t('user_status', 'Away')
-                                       }
-                                       if (item.status === 'dnd') {
-                                               message = t('user_status', 'Do not disturb')
-                                       }
-                               }
-                               const status = item.icon !== '' ? `${icon} ${message}` : message
-
-                               let subText
-                               if (item.icon === null && message === '' && item.timestamp === null) {
-                                       subText = ''
-                               } else if (item.icon === null && message === '' && item.timestamp !== null) {
-                                       subText = moment(item.timestamp, 'X').fromNow()
-                               } else if (item.timestamp !== null) {
-                                       subText = this.t('user_status', '{status}, {timestamp}', {
-                                               status,
-                                               timestamp: moment(item.timestamp, 'X').fromNow(),
-                                       }, null, { escape: false, sanitize: false })
-                               } else {
-                                       subText = status
-                               }
-
-                               return {
-                                       mainText: item.displayName,
-                                       subText,
-                                       avatarUsername: item.userId,
-                               }
-                       })
-               },
-       },
-       mounted() {
-               try {
-                       this.statuses = loadState('user_status', 'dashboard_data')
-                       this.loading = false
-               } catch (e) {
-                       console.error(e)
-               }
-       },
-}
-</script>
-
-<style lang="scss">
-.icon-user-status-dark {
-       width: 64px;
-       height: 64px;
-       background-size: 64px;
-       filter: var(--background-invert-if-dark);
-}
-</style>
index 5481325510c5e135b5205bb71837aca01163bd98..9e1c39f68584caa40ac6abe4616d6ab0e25ab522 100644 (file)
@@ -7,6 +7,7 @@ declare(strict_types=1);
  *
  * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  * @author Georg Ehrke <oc.list@georgehrke.com>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -27,13 +28,11 @@ declare(strict_types=1);
 namespace OCA\UserStatus\Tests\Dashboard;
 
 use OCA\UserStatus\Dashboard\UserStatusWidget;
-use OCA\UserStatus\Db\UserStatus;
 use OCA\UserStatus\Service\StatusService;
 use OCP\AppFramework\Services\IInitialState;
 use OCP\IDateTimeFormatter;
 use OCP\IL10N;
 use OCP\IURLGenerator;
-use OCP\IUser;
 use OCP\IUserManager;
 use OCP\IUserSession;
 use Test\TestCase;
@@ -101,172 +100,4 @@ class UserStatusWidgetTest extends TestCase {
        public function testGetUrl(): void {
                $this->assertNull($this->widget->getUrl());
        }
-
-       public function testLoadNoUserSession(): void {
-               $this->userSession->expects($this->once())
-                       ->method('getUser')
-                       ->willReturn(null);
-
-               $this->initialState->expects($this->once())
-                       ->method('provideInitialState')
-                       ->with('dashboard_data', []);
-
-               $this->service->expects($this->never())
-                       ->method('findAllRecentStatusChanges');
-
-               $this->widget->load();
-       }
-
-       public function testLoadWithCurrentUser(): void {
-               $user = $this->createMock(IUser::class);
-               $user->method('getUid')->willReturn('john.doe');
-               $this->userSession->expects($this->once())
-                       ->method('getUser')
-                       ->willReturn($user);
-
-               $user1 = $this->createMock(IUser::class);
-               $user1->method('getDisplayName')->willReturn('User No. 1');
-
-               $this->userManager
-                       ->method('get')
-                       ->willReturnMap([
-                               ['user_1', $user1],
-                               ['user_2', null],
-                               ['user_3', null],
-                               ['user_4', null],
-                               ['user_5', null],
-                               ['user_6', null],
-                               ['user_7', null],
-                       ]);
-
-               $userStatuses = [
-                       UserStatus::fromParams([
-                               'userId' => 'user_1',
-                               'status' => 'online',
-                               'customIcon' => '💻',
-                               'customMessage' => 'Working',
-                               'statusTimestamp' => 5000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'user_2',
-                               'status' => 'away',
-                               'customIcon' => '☕️',
-                               'customMessage' => 'Office Hangout',
-                               'statusTimestamp' => 6000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'user_3',
-                               'status' => 'dnd',
-                               'customIcon' => null,
-                               'customMessage' => null,
-                               'statusTimestamp' => 7000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'john.doe',
-                               'status' => 'away',
-                               'customIcon' => '☕️',
-                               'customMessage' => 'Office Hangout',
-                               'statusTimestamp' => 90000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'user_4',
-                               'status' => 'dnd',
-                               'customIcon' => null,
-                               'customMessage' => null,
-                               'statusTimestamp' => 7000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'user_5',
-                               'status' => 'invisible',
-                               'customIcon' => '🏝',
-                               'customMessage' => 'On vacation',
-                               'statusTimestamp' => 7000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'user_6',
-                               'status' => 'offline',
-                               'customIcon' => null,
-                               'customMessage' => null,
-                               'statusTimestamp' => 7000,
-                       ]),
-                       UserStatus::fromParams([
-                               'userId' => 'user_7',
-                               'status' => 'invisible',
-                               'customIcon' => null,
-                               'customMessage' => null,
-                               'statusTimestamp' => 7000,
-                       ]),
-               ];
-
-               $this->service->expects($this->once())
-                       ->method('findAllRecentStatusChanges')
-                       ->with(8, 0)
-                       ->willReturn($userStatuses);
-
-               $this->initialState->expects($this->once())
-                       ->method('provideInitialState')
-                       ->with('dashboard_data', $this->callback(function ($data): bool {
-                               $this->assertEquals([
-                                       [
-                                               'userId' => 'user_1',
-                                               'displayName' => 'User No. 1',
-                                               'status' => 'online',
-                                               'icon' => '💻',
-                                               'message' => 'Working',
-                                               'timestamp' => 5000,
-                                       ],
-                                       [
-                                               'userId' => 'user_2',
-                                               'displayName' => 'user_2',
-                                               'status' => 'away',
-                                               'icon' => '☕️',
-                                               'message' => 'Office Hangout',
-                                               'timestamp' => 6000,
-                                       ],
-                                       [
-                                               'userId' => 'user_3',
-                                               'displayName' => 'user_3',
-                                               'status' => 'dnd',
-                                               'icon' => null,
-                                               'message' => null,
-                                               'timestamp' => 7000,
-                                       ],
-                                       [
-                                               'userId' => 'user_4',
-                                               'displayName' => 'user_4',
-                                               'status' => 'dnd',
-                                               'icon' => null,
-                                               'message' => null,
-                                               'timestamp' => 7000,
-                                       ],
-                                       [
-                                               'userId' => 'user_5',
-                                               'displayName' => 'user_5',
-                                               'status' => 'offline',
-                                               'icon' => '🏝',
-                                               'message' => 'On vacation',
-                                               'timestamp' => 7000,
-                                       ],
-                                       [
-                                               'userId' => 'user_6',
-                                               'displayName' => 'user_6',
-                                               'status' => 'offline',
-                                               'icon' => null,
-                                               'message' => null,
-                                               'timestamp' => 7000,
-                                       ],
-                                       [
-                                               'userId' => 'user_7',
-                                               'displayName' => 'user_7',
-                                               'status' => 'offline',
-                                               'icon' => null,
-                                               'message' => null,
-                                               'timestamp' => 7000,
-                                       ],
-                               ], $data);
-                               return true;
-                       }));
-
-               $this->widget->load();
-       }
 }
index ecf3466392f7d6153e7c081fb0a798de82f14fb2..b8c864abe4dc3114a9b9b18e4b6a9bf7c76c4c75 100644 (file)
@@ -217,14 +217,17 @@ return array(
     'OCP\\DB\\QueryBuilder\\IQueryFunction' => $baseDir . '/lib/public/DB/QueryBuilder/IQueryFunction.php',
     'OCP\\DB\\Types' => $baseDir . '/lib/public/DB/Types.php',
     'OCP\\Dashboard\\IAPIWidget' => $baseDir . '/lib/public/Dashboard/IAPIWidget.php',
+    'OCP\\Dashboard\\IAPIWidgetV2' => $baseDir . '/lib/public/Dashboard/IAPIWidgetV2.php',
     'OCP\\Dashboard\\IButtonWidget' => $baseDir . '/lib/public/Dashboard/IButtonWidget.php',
     'OCP\\Dashboard\\IConditionalWidget' => $baseDir . '/lib/public/Dashboard/IConditionalWidget.php',
     'OCP\\Dashboard\\IIconWidget' => $baseDir . '/lib/public/Dashboard/IIconWidget.php',
     'OCP\\Dashboard\\IManager' => $baseDir . '/lib/public/Dashboard/IManager.php',
     'OCP\\Dashboard\\IOptionWidget' => $baseDir . '/lib/public/Dashboard/IOptionWidget.php',
+    'OCP\\Dashboard\\IReloadableWidget' => $baseDir . '/lib/public/Dashboard/IReloadableWidget.php',
     'OCP\\Dashboard\\IWidget' => $baseDir . '/lib/public/Dashboard/IWidget.php',
     'OCP\\Dashboard\\Model\\WidgetButton' => $baseDir . '/lib/public/Dashboard/Model/WidgetButton.php',
     'OCP\\Dashboard\\Model\\WidgetItem' => $baseDir . '/lib/public/Dashboard/Model/WidgetItem.php',
+    'OCP\\Dashboard\\Model\\WidgetItems' => $baseDir . '/lib/public/Dashboard/Model/WidgetItems.php',
     'OCP\\Dashboard\\Model\\WidgetOptions' => $baseDir . '/lib/public/Dashboard/Model/WidgetOptions.php',
     'OCP\\Dashboard\\RegisterWidgetEvent' => $baseDir . '/lib/public/Dashboard/RegisterWidgetEvent.php',
     'OCP\\DataCollector\\AbstractDataCollector' => $baseDir . '/lib/public/DataCollector/AbstractDataCollector.php',
index cbfb6bc018982543d6bd9ef285e56467a2c0d2e7..e099accf68375499043e2131ac5fafa76cc8f662 100644 (file)
@@ -250,14 +250,17 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\DB\\QueryBuilder\\IQueryFunction' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IQueryFunction.php',
         'OCP\\DB\\Types' => __DIR__ . '/../../..' . '/lib/public/DB/Types.php',
         'OCP\\Dashboard\\IAPIWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IAPIWidget.php',
+        'OCP\\Dashboard\\IAPIWidgetV2' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IAPIWidgetV2.php',
         'OCP\\Dashboard\\IButtonWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IButtonWidget.php',
         'OCP\\Dashboard\\IConditionalWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IConditionalWidget.php',
         'OCP\\Dashboard\\IIconWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IIconWidget.php',
         'OCP\\Dashboard\\IManager' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IManager.php',
         'OCP\\Dashboard\\IOptionWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IOptionWidget.php',
+        'OCP\\Dashboard\\IReloadableWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IReloadableWidget.php',
         'OCP\\Dashboard\\IWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IWidget.php',
         'OCP\\Dashboard\\Model\\WidgetButton' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetButton.php',
         'OCP\\Dashboard\\Model\\WidgetItem' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetItem.php',
+        'OCP\\Dashboard\\Model\\WidgetItems' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetItems.php',
         'OCP\\Dashboard\\Model\\WidgetOptions' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetOptions.php',
         'OCP\\Dashboard\\RegisterWidgetEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterWidgetEvent.php',
         'OCP\\DataCollector\\AbstractDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/AbstractDataCollector.php',
diff --git a/lib/public/Dashboard/IAPIWidgetV2.php b/lib/public/Dashboard/IAPIWidgetV2.php
new file mode 100644 (file)
index 0000000..27cb651
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Dashboard;
+
+use OCP\Dashboard\Model\WidgetItems;
+
+/**
+ * Interface IAPIWidgetV2
+ *
+ * @since 27.1.0
+ */
+interface IAPIWidgetV2 extends IWidget {
+       /**
+        * Items to render in the widget
+        *
+        * @since 27.1.0
+        */
+       public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems;
+}
diff --git a/lib/public/Dashboard/IReloadableWidget.php b/lib/public/Dashboard/IReloadableWidget.php
new file mode 100644 (file)
index 0000000..9f65653
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Dashboard;
+
+/**
+ * Allow {@see IAPIWidgetV2} to reload their items
+ *
+ * @since 27.1.0
+ */
+interface IReloadableWidget extends IAPIWidgetV2 {
+       /**
+        * Periodic interval in seconds in which to reload the widget's items
+        *
+        * @since 27.1.0
+        */
+       public function getReloadInterval(): int;
+}
index 859a5652351936f61a087fb1770bed525ae56edd..1c54d2bd426fbf67676fe7047360f2090c391a5d 100644 (file)
@@ -6,6 +6,7 @@ declare(strict_types=1);
  * @copyright 2021, Julien Veyssier <eneiluj@posteo.net>
  *
  * @author Julien Veyssier <eneiluj@posteo.net>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -56,24 +57,30 @@ final class WidgetItem implements JsonSerializable {
         */
        private $sinceId = '';
 
+       /**
+        * Overlay icon to show in the bottom right corner of {@see $iconUrl}
+        *
+        * @since 27.1.0
+        */
+       private string $overlayIconUrl = '';
 
        /**
         * WidgetItem constructor
         *
         * @since 22.0.0
-        *
-        * @param string $type
         */
        public function __construct(string $title = '',
                                                                string $subtitle = '',
                                                                string $link = '',
                                                                string $iconUrl = '',
-                                                               string $sinceId = '') {
+                                                               string $sinceId = '',
+                                                               string $overlayIconUrl = '') {
                $this->title = $title;
                $this->subtitle = $subtitle;
                $this->iconUrl = $iconUrl;
                $this->link = $link;
                $this->sinceId = $sinceId;
+               $this->overlayIconUrl = $overlayIconUrl;
        }
 
        /**
@@ -132,6 +139,17 @@ final class WidgetItem implements JsonSerializable {
                return $this->sinceId;
        }
 
+       /**
+        * Get the overlay icon url
+        *
+        * @since 27.1.0
+        *
+        * @return string
+        */
+       public function getOverlayIconUrl(): string {
+               return $this->overlayIconUrl;
+       }
+
        /**
         * @since 22.0.0
         *
@@ -143,6 +161,7 @@ final class WidgetItem implements JsonSerializable {
                        'title' => $this->getTitle(),
                        'link' => $this->getLink(),
                        'iconUrl' => $this->getIconUrl(),
+                       'overlayIconUrl' => $this->getOverlayIconUrl(),
                        'sinceId' => $this->getSinceId(),
                ];
        }
diff --git a/lib/public/Dashboard/Model/WidgetItems.php b/lib/public/Dashboard/Model/WidgetItems.php
new file mode 100644 (file)
index 0000000..4bb51f2
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Dashboard\Model;
+
+use JsonSerializable;
+use OCP\Dashboard\IAPIWidgetV2;
+
+/**
+ * Interface WidgetItems
+ *
+ * This class is used by {@see IAPIWidgetV2} interface.
+ * It represents an array of widget items and additional context information that can be provided to clients via the Dashboard API
+ *
+ * @see IAPIWidgetV2::getItemsV2
+ *
+ * @since 27.1.0
+ */
+class WidgetItems implements JsonSerializable {
+       /**
+        * @param $items WidgetItem[]
+        *
+        * @since 27.1.0
+        */
+       public function __construct(
+               private array $items = [],
+               private string $emptyContentMessage = '',
+               private string $halfEmptyContentMessage = '',
+       ) {
+       }
+
+       /**
+        * Items to render in the widgets
+        *
+        * @since 27.1.0
+        *
+        * @return WidgetItem[]
+        */
+       public function getItems(): array {
+               return $this->items;
+       }
+
+       /**
+        * The "half" empty content message to show above the list of items.
+        *
+        * A non-empty string enables this feature.
+        * An empty string hides the message and disables this feature.
+        *
+        * @since 27.1.0
+        */
+       public function getEmptyContentMessage(): string {
+               return $this->emptyContentMessage;
+       }
+
+       /**
+        * The empty content message to show in case of no items at all
+        *
+        * @since 27.1.0
+        */
+       public function getHalfEmptyContentMessage(): string {
+               return $this->halfEmptyContentMessage;
+       }
+
+       /**
+        * @since 27.1.0
+        */
+       public function jsonSerialize(): array {
+               $items = array_map(static function (WidgetItem $item) {
+                       return $item->jsonSerialize();
+               }, $this->getItems());
+               return [
+                       'items' => $items,
+                       'emptyContentMessage' => $this->getEmptyContentMessage(),
+                       'halfEmptyContentMessage' => $this->getHalfEmptyContentMessage(),
+               ];
+       }
+}
index 72509887a5fc31b375eb1cf63937944ac538f322..cc7f5b7829afac3de9d3ad46d1f21071d23bb4ca 100644 (file)
@@ -109,7 +109,6 @@ module.exports = {
                updatenotification: path.join(__dirname, 'apps/updatenotification/src', 'init.js'),
        },
        user_status: {
-               dashboard: path.join(__dirname, 'apps/user_status/src', 'dashboard.js'),
                menu: path.join(__dirname, 'apps/user_status/src', 'menu.js'),
        },
        weather_status: {