*
* @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
*
'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'],
]
];
* @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
*
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;
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 {
* 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
* @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));
}
*
* @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();
'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 += [
}, $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);
+++ /dev/null
-{
- "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
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'
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')
export default {
name: 'DashboardApp',
components: {
+ ApiDashboardWidget,
NcButton,
Draggable,
NcModal,
modal: false,
appStoreUrl: generateUrl('/settings/apps/dashboard'),
statuses: {},
+ apiWidgets: [],
+ apiWidgetItems: {},
+ loadingItems: true,
}
},
computed: {
},
},
+ 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)
},
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
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>
margin-right: 16px;
background-position: center;
float: left;
+ margin-top: -6px;
}
}
}
--- /dev/null
+<!--
+ - @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>
* @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
*
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;
*
* @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;
* @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 {
}, $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);
}
+++ /dev/null
-/**
- * @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)
- })
-
-})
+++ /dev/null
-<!--
- - @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>
*
* @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
*
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;
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();
- }
}
'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',
'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',
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
* @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
*
*/
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;
}
/**
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
*
'title' => $this->getTitle(),
'link' => $this->getLink(),
'iconUrl' => $this->getIconUrl(),
+ 'overlayIconUrl' => $this->getOverlayIconUrl(),
'sinceId' => $this->getSinceId(),
];
}
--- /dev/null
+<?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(),
+ ];
+ }
+}
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: {