Skip to content

Commit

Permalink
Merge pull request #49534 from nextcloud/feature/23308/create-new-fav…
Browse files Browse the repository at this point in the history
…orite-dashboard-widget

feature: added new FavouriteWidget to display favorite files in dashboard widget
  • Loading branch information
sorbaugh authored Jan 15, 2025
2 parents eef5663 + 6da691d commit 0f2dcfd
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 29 deletions.
4 changes: 2 additions & 2 deletions apps/dashboard/src/DashboardApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -461,8 +461,8 @@ export default {
}
},
async fetchApiWidgets() {
const response = await axios.get(generateOcsUrl('/apps/dashboard/api/v1/widgets'))
this.apiWidgets = response.data.ocs.data
const { data } = await axios.get(generateOcsUrl('/apps/dashboard/api/v1/widgets'))
this.apiWidgets = data.ocs.data
},
async fetchApiWidgetItems(widgetIds, merge = false) {
try {
Expand Down
29 changes: 14 additions & 15 deletions apps/dashboard/src/components/ApiDashboardWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,7 @@
: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>
<ApiDashboardWidgetItem :item="item" :icon-size="iconSize" :rounded-icons="widget.item_icons_round" />
</template>
<template #empty-content>
<NcEmptyContent v-if="items.length === 0"
Expand All @@ -39,23 +30,21 @@

<script>
import {
NcAvatar,
NcDashboardWidget,
NcDashboardWidgetItem,
NcEmptyContent,
NcButton,
} from '@nextcloud/vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import ApiDashboardWidgetItem from './ApiDashboardWidgetItem.vue'

export default {
name: 'ApiDashboardWidget',
components: {
NcAvatar,
ApiDashboardWidgetItem,
CheckIcon,
NcDashboardWidget,
NcDashboardWidgetItem,
NcEmptyContent,
NcButton,
CheckIcon,
},
props: {
widget: {
Expand All @@ -71,6 +60,11 @@ export default {
required: true,
},
},
data() {
return {
iconSize: 44,
}
},
computed: {
/** @return {object[]} */
items() {
Expand Down Expand Up @@ -115,5 +109,10 @@ export default {
return this.moreButton?.link
},
},
mounted() {
const size = window.getComputedStyle(document.body).getPropertyValue('--default-clickable-area')
const numeric = Number.parseFloat(size)
this.iconSize = Number.isNaN(numeric) ? 44 : numeric
},
}
</script>
68 changes: 68 additions & 0 deletions apps/dashboard/src/components/ApiDashboardWidgetItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { ref } from 'vue'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcDashboardWidgetItem from '@nextcloud/vue/dist/Components/NcDashboardWidgetItem.js'
import IconFile from 'vue-material-design-icons/File.vue'

defineProps({
item: {
type: Object,
required: true,
},
iconSize: {
type: Number,
required: true,
},
roundedIcons: {
type: Boolean,
default: true,
},
})

/**
* True as soon as the image is loaded
*/
const imageLoaded = ref(false)
/**
* True if the image failed to load and we should show a fallback
*/
const loadingImageFailed = ref(false)
</script>

<template>
<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 v-if="roundedIcons"
:size="iconSize"
:url="item.iconUrl" />
<template v-else>
<img v-show="!loadingImageFailed"
alt=""
class="api-dashboard-widget-item__icon"
:class="{'hidden-visually': !imageLoaded }"
:src="item.iconUrl"
@error="loadingImageFailed = true"
@load="imageLoaded = true">
<!-- Placeholder while the image is loaded and also the fallback if the URL is broken -->
<IconFile v-if="!imageLoaded"
:size="iconSize" />
</template>
</template>
</template>
</NcDashboardWidgetItem>
</template>

<style scoped>
.api-dashboard-widget-item__icon {
height: var(--default-clickable-area);
width: var(--default-clickable-area);
}
</style>
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
'OCA\\Files\\Dashboard\\FavoriteWidget' => $baseDir . '/../lib/Dashboard/FavoriteWidget.php',
'OCA\\Files\\Db\\OpenLocalEditor' => $baseDir . '/../lib/Db/OpenLocalEditor.php',
'OCA\\Files\\Db\\OpenLocalEditorMapper' => $baseDir . '/../lib/Db/OpenLocalEditorMapper.php',
'OCA\\Files\\Db\\TransferOwnership' => $baseDir . '/../lib/Db/TransferOwnership.php',
Expand Down
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
'OCA\\Files\\Dashboard\\FavoriteWidget' => __DIR__ . '/..' . '/../lib/Dashboard/FavoriteWidget.php',
'OCA\\Files\\Db\\OpenLocalEditor' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditor.php',
'OCA\\Files\\Db\\OpenLocalEditorMapper' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditorMapper.php',
'OCA\\Files\\Db\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Db/TransferOwnership.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OCA\Files\Collaboration\Resources\Listener;
use OCA\Files\Collaboration\Resources\ResourceProvider;
use OCA\Files\Controller\ApiController;
use OCA\Files\Dashboard\FavoriteWidget;
use OCA\Files\DirectEditingCapabilities;
use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
Expand Down Expand Up @@ -123,6 +124,7 @@ public function register(IRegistrationContext $context): void {
$context->registerSearchProvider(FilesSearchProvider::class);

$context->registerNotifierService(Notifier::class);
$context->registerDashboardWidget(FavoriteWidget::class);
}

public function boot(IBootContext $context): void {
Expand Down
136 changes: 136 additions & 0 deletions apps/files/lib/Dashboard/FavoriteWidget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files\Dashboard;

use OCA\Files\AppInfo\Application;
use OCP\Dashboard\IAPIWidgetV2;
use OCP\Dashboard\IButtonWidget;
use OCP\Dashboard\IIconWidget;
use OCP\Dashboard\IOptionWidget;
use OCP\Dashboard\Model\WidgetButton;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetItems;
use OCP\Dashboard\Model\WidgetOptions;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\IPreview;
use OCP\ITagManager;
use OCP\IURLGenerator;
use OCP\IUserManager;

class FavoriteWidget implements IIconWidget, IAPIWidgetV2, IButtonWidget, IOptionWidget {

public function __construct(
private readonly IL10N $l10n,
private readonly IURLGenerator $urlGenerator,
private readonly IMimeTypeDetector $mimeTypeDetector,
private readonly IUserManager $userManager,
private readonly ITagManager $tagManager,
private readonly IRootFolder $rootFolder,
private readonly IPreview $previewManager,
) {
}

public function getId(): string {
return Application::APP_ID . '-favorites';
}

public function getTitle(): string {
return $this->l10n->t('Favorite files');
}

public function getOrder(): int {
return 0;
}

public function getIconClass(): string {
return 'icon-star-dark';
}

public function getIconUrl(): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath('core', 'actions/star.svg')
);
}

public function getUrl(): ?string {
return $this->urlGenerator->linkToRouteAbsolute('files.View.indexView', ['view' => 'favorites']);
}

public function load(): void {
}

public function getItems(string $userId, int $limit = 7): array {
$user = $this->userManager->get($userId);

if (!$user) {
return [];
}
$tags = $this->tagManager->load('files', [], false, $userId);
$favorites = $tags->getFavorites();
if (empty($favorites)) {
return [];
}
$favoriteNodes = [];
$userFolder = $this->rootFolder->getUserFolder($userId);
$count = 0;
foreach ($favorites as $favorite) {
$node = $userFolder->getFirstNodeById($favorite);
if ($node) {
$url = $this->urlGenerator->linkToRouteAbsolute(
'files.view.showFile', ['fileid' => $node->getId()]
);
$icon = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', [
'x' => 256,
'y' => 256,
'fileId' => $node->getId(),
'c' => $node->getEtag(),
'mimeFallback' => true,
]);
$favoriteNodes[] = new WidgetItem(
$node->getName(),
'',
$url,
$icon,
(string)$node->getCreationTime()
);
$count++;
if ($count >= $limit) {
break;
}
}
}

return $favoriteNodes;
}

public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems {
$items = $this->getItems($userId, $limit);
return new WidgetItems(
$items,
count($items) === 0 ? $this->l10n->t('No favorites') : '',
);
}

public function getWidgetButtons(string $userId): array {
return [
new WidgetButton(
WidgetButton::TYPE_MORE,
$this->urlGenerator->linkToRouteAbsolute('files.View.indexView', ['view' => 'favorites']),
$this->l10n->t('More favorites')
),
];
}

public function getWidgetOptions(): WidgetOptions {
return new WidgetOptions(roundItemIcons: false);
}
}
4 changes: 2 additions & 2 deletions dist/core-common.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-common.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/dashboard-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/dashboard-main.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/files-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-main.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/files-reference-files.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-reference-files.js.map

Large diffs are not rendered by default.

0 comments on commit 0f2dcfd

Please sign in to comment.