Merge pull request #39937 from nextcloud/feat/dashboard/item-api-v2

feat(dashboard): implement widget item api v2
pull/39950/head
Joas Schilling 2 years ago committed by GitHub
commit 5234807c60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/dashboard/appinfo/routes.php
  2. 72
      apps/dashboard/lib/Controller/DashboardApiController.php
  3. 10
      apps/dashboard/lib/ResponseDefinitions.php
  4. 134
      apps/dashboard/openapi.json
  5. 106
      apps/dashboard/src/DashboardApp.vue
  6. 140
      apps/dashboard/src/components/ApiDashboardWidget.vue
  7. 27
      apps/user_status/lib/Dashboard/UserStatusWidget.php
  8. 44
      apps/user_status/src/dashboard.js
  9. 121
      apps/user_status/src/views/Dashboard.vue
  10. 171
      apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php
  11. 4
      dist/2798-2798.js
  12. 2
      dist/2798-2798.js.map
  13. 4
      dist/614-614.js
  14. 2
      dist/614-614.js.map
  15. 4
      dist/core-common.js
  16. 2
      dist/core-common.js.map
  17. 4
      dist/core-unsupported-browser.js
  18. 2
      dist/core-unsupported-browser.js.map
  19. 4
      dist/dashboard-main.js
  20. 2
      dist/dashboard-main.js.LICENSE.txt
  21. 2
      dist/dashboard-main.js.map
  22. 4
      dist/dav-settings-personal-availability.js
  23. 2
      dist/dav-settings-personal-availability.js.map
  24. 4
      dist/files-main.js
  25. 2
      dist/files-main.js.map
  26. 4
      dist/files_sharing-files_sharing_tab.js
  27. 2
      dist/files_sharing-files_sharing_tab.js.map
  28. 4
      dist/settings-users-8351.js
  29. 2
      dist/settings-users-8351.js.map
  30. 4
      dist/settings-vue-settings-apps-users-management.js
  31. 2
      dist/settings-vue-settings-apps-users-management.js.map
  32. 4
      dist/settings-vue-settings-personal-info.js
  33. 2
      dist/settings-vue-settings-personal-info.js.map
  34. 3
      dist/user_status-dashboard.js
  35. 25
      dist/user_status-dashboard.js.LICENSE.txt
  36. 1
      dist/user_status-dashboard.js.map
  37. 3
      lib/composer/composer/autoload_classmap.php
  38. 3
      lib/composer/composer/autoload_static.php
  39. 43
      lib/public/Dashboard/IAPIWidgetV2.php
  40. 41
      lib/public/Dashboard/IReloadableWidget.php
  41. 25
      lib/public/Dashboard/Model/WidgetItem.php
  42. 100
      lib/public/Dashboard/Model/WidgetItems.php
  43. 1
      webpack.modules.js

@ -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'],
]
];

@ -7,6 +7,7 @@ declare(strict_types=1);
*
* @author Julien Veyssier <eneiluj@posteo.net>
* @author Kate Döen <kate.doeen@nextcloud.com>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
@ -35,6 +36,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;
@ -42,11 +44,14 @@ use OCP\IConfig;
use OCP\IRequest;
use OCP\Dashboard\IAPIWidget;
use OCP\Dashboard\IAPIWidgetV2;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetItems;
/**
* @psalm-import-type DashboardWidget from ResponseDefinitions
* @psalm-import-type DashboardWidgetItem from ResponseDefinitions
* @psalm-import-type DashboardWidgetItems from ResponseDefinitions
*/
class DashboardApiController extends OCSController {
@ -71,6 +76,24 @@ class DashboardApiController extends OCSController {
$this->userId = $userId;
}
/**
* @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);
},
);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -83,18 +106,11 @@ class DashboardApiController extends OCSController {
* @return DataResponse<Http::STATUS_OK, array<string, DashboardWidgetItem[]>, array{}>
*/
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));
}
@ -103,6 +119,31 @@ class DashboardApiController extends OCSController {
return new DataResponse($items);
}
/**
* @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
*
@ -124,6 +165,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 += [
@ -136,6 +179,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);

@ -5,6 +5,7 @@ declare(strict_types=1);
* @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com>
*
* @author Kate Döen <kate.doeen@nextcloud.com>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
@ -34,6 +35,8 @@ namespace OCA\Dashboard;
* icon_url: string,
* widget_url: ?string,
* item_icons_round: bool,
* item_api_versions: int[],
* reload_interval: int,
* buttons?: array{
* type: string,
* text: string,
@ -46,8 +49,15 @@ namespace OCA\Dashboard;
* title: string,
* link: string,
* iconUrl: string,
* overlayIconUrl: string,
* sinceId: string,
* }
*
* @psalm-type DashboardWidgetItems = array{
* items: DashboardWidgetItem[],
* emptyContentMessage: string,
* halfEmptyContentMessage: string,
* }
*/
class ResponseDefinitions {
}

@ -53,7 +53,9 @@
"icon_class",
"icon_url",
"widget_url",
"item_icons_round"
"item_icons_round",
"item_api_versions",
"reload_interval"
],
"properties": {
"id": {
@ -79,6 +81,17 @@
"item_icons_round": {
"type": "boolean"
},
"item_api_versions": {
"type": "array",
"items": {
"type": "integer",
"format": "int64"
}
},
"reload_interval": {
"type": "integer",
"format": "int64"
},
"buttons": {
"type": "array",
"items": {
@ -110,6 +123,7 @@
"title",
"link",
"iconUrl",
"overlayIconUrl",
"sinceId"
],
"properties": {
@ -125,10 +139,35 @@
"iconUrl": {
"type": "string"
},
"overlayIconUrl": {
"type": "string"
},
"sinceId": {
"type": "string"
}
}
},
"WidgetItems": {
"type": "object",
"required": [
"items",
"emptyContentMessage",
"halfEmptyContentMessage"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WidgetItem"
}
},
"emptyContentMessage": {
"type": "string"
},
"halfEmptyContentMessage": {
"type": "string"
}
}
}
}
},
@ -291,6 +330,99 @@
}
}
}
},
"/ocs/v2.php/apps/dashboard/api/v2/widget-items": {
"get": {
"operationId": "dashboard_api-get-widget-items-v2",
"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": "array",
"default": [],
"items": {
"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": {
"$ref": "#/components/schemas/WidgetItems"
}
}
}
}
}
}
}
}
}
}
}
}
},
"tags": []

@ -14,21 +14,44 @@
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">
@ -94,7 +117,7 @@
</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;
}
}
}

@ -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>

@ -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);
}

@ -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)
})
})

@ -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>

@ -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();
}
}

4
dist/2798-2798.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/614-614.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,3 +1,5 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,25 +0,0 @@
/*! For license information please see NcDashboardWidget.js.LICENSE.txt */
/*! For license information please see NcDashboardWidgetItem.js.LICENSE.txt */
/**
* @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/>.
*
*/

File diff suppressed because one or more lines are too long

@ -228,14 +228,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',

@ -261,14 +261,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',

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

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

@ -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(),
];
}

@ -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(),
];
}
}

@ -112,7 +112,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: {

Loading…
Cancel
Save