mirror of
https://github.com/nextcloud/server.git
synced 2026-03-24 03:13:36 -04:00
Merge pull request #49534 from nextcloud/feature/23308/create-new-favorite-dashboard-widget
feature: added new FavouriteWidget to display favorite files in dashboard widget
This commit is contained in:
commit
0f2dcfd0f1
15 changed files with 236 additions and 29 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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: {
|
||||
|
|
@ -71,6 +60,11 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iconSize: 44,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {object[]} */
|
||||
items() {
|
||||
|
|
@ -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
apps/dashboard/src/components/ApiDashboardWidgetItem.vue
Normal file
68
apps/dashboard/src/components/ApiDashboardWidgetItem.vue
Normal file
|
|
@ -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>
|
||||
|
|
@ -48,6 +48,7 @@ return array(
|
|||
'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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use OCA\Files\Capabilities;
|
|||
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;
|
||||
|
|
@ -123,6 +124,7 @@ class Application extends App implements IBootstrap {
|
|||
$context->registerSearchProvider(FilesSearchProvider::class);
|
||||
|
||||
$context->registerNotifierService(Notifier::class);
|
||||
$context->registerDashboardWidget(FavoriteWidget::class);
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
|
|
|
|||
136
apps/files/lib/Dashboard/FavoriteWidget.php
Normal file
136
apps/files/lib/Dashboard/FavoriteWidget.php
Normal file
|
|
@ -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
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dashboard-main.js
vendored
4
dist/dashboard-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/dashboard-main.js.map
vendored
2
dist/dashboard-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-reference-files.js
vendored
4
dist/files-reference-files.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-reference-files.js.map
vendored
2
dist/files-reference-files.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue