TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:14 +02:00
commit ff4622ba97
138 changed files with 9045 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/vendor/
@@ -0,0 +1,458 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Dto\Settings\EditableSetting;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\Category;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsDiff;
use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
use TYPO3\CMS\Dashboard\DashboardPreset;
use TYPO3\CMS\Dashboard\DashboardPresetRegistry;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\Repository\DashboardRepository;
use TYPO3\CMS\Dashboard\WidgetGroupInitializationService;
use TYPO3\CMS\Dashboard\WidgetRegistry;
/**
* @internal
*/
#[AsController]
readonly class DashboardAjaxController
{
public function __construct(
protected DashboardRepository $dashboardRepository,
protected DashboardPresetRegistry $dashboardPresetRegistry,
protected WidgetRegistry $widgetRegistry,
protected WidgetGroupInitializationService $widgetGroupInitializationService,
protected WidgetSettingsFactory $widgetSettingsFactory,
protected SettingsTypeRegistry $settingsTypeRegistry,
protected UriBuilder $uriBuilder,
) {}
public function getDashboards(ServerRequestInterface $request): ResponseInterface
{
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboards = [];
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
$dashboards[] = $dashboard->getTransferData();
}
return new JsonResponse($dashboards);
}
public function addDashboard(ServerRequestInterface $request): ResponseInterface
{
$presetIdentifier = (string)($request->getParsedBody()['preset'] ?? '');
$dashboardPreset = $this->dashboardPresetRegistry->getDashboardPresets()[$presetIdentifier] ?? null;
if (!$dashboardPreset instanceof DashboardPreset) {
return new JsonResponse([
'status' => 'error',
'message' => 'Invalid dashboard preset!',
]);
}
$dashboardEntity = $this->dashboardRepository->create(
$dashboardPreset,
(int)$this->getBackendUser()->user['uid'],
(string)($request->getParsedBody()['title'] ?? '')
);
$dashboardEntity->initializeWidgets($request);
return new JsonResponse([
'status' => 'ok',
'dashboard' => $dashboardEntity->getTransferData(),
]);
}
public function editDashboard(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboardEntity, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$this->dashboardRepository->updateDashboardSettings(
$dashboardIdentifier,
[
'title' => (string)($request->getParsedBody()['title'] ?? ''),
]
);
// Fetch updated Dashboard
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
$dashboardEntity->initializeWidgets($request);
return new JsonResponse([
'status' => 'ok',
'dashboard' => $dashboardEntity->getTransferData(),
]);
}
public function updateDashboard(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboardEntity, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$widgets = $request->getParsedBody()['widgets'] ?? [];
$data = [];
foreach ($widgets as $widget) {
$data[$widget['identifier']] = [
'identifier' => $widget['type'],
];
}
// positions
$widgetPositions = $request->getParsedBody()['widgetPositions'] ?? [];
foreach ($widgetPositions as $columnCount => $widgets) {
foreach ($widgets as $widget) {
if (!isset($widget['identifier']) || !isset($widget['height']) || !isset($widget['width']) || !isset($widget['x']) || !isset($widget['y'])) {
return new JsonResponse([
'status' => 'error',
'message' => 'Invalid widget positions!',
]);
}
$identifier = $widget['identifier'] ?? '';
unset($widget['identifier']);
$data[$identifier]['positions'][$columnCount] = array_map('intval', $widget);
}
}
// settings
$dashboardEntity->initializeWidgets($request);
foreach ($widgets as $widget) {
$dashboardWidget = $dashboardEntity->getWidget($widget['identifier']);
if ($dashboardWidget) {
$data[$widget['identifier']]['settings'] = $dashboardWidget->getRawConfig()['settings'] ?? [];
}
}
$this->dashboardRepository->updateWidgetConfig($dashboardEntity, $data);
// Fetch updated Dashboard
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
$dashboardEntity->initializeWidgets($request);
return new JsonResponse([
'status' => 'ok',
'dashboard' => $dashboardEntity->getTransferData(),
]);
}
public function deleteDashboard(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboard, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$this->dashboardRepository->delete($dashboard);
return new JsonResponse([
'status' => 'ok',
]);
}
public function getPresets(ServerRequestInterface $request): ResponseInterface
{
$presets = $this->dashboardPresetRegistry->getDashboardPresets();
return new JsonResponse($presets);
}
public function getCategories(ServerRequestInterface $request): ResponseInterface
{
$widgetGroups = $this->widgetGroupInitializationService->buildWidgetGroupsConfiguration();
return new JsonResponse($widgetGroups);
}
public function getWidget(ServerRequestInterface $request): ResponseInterface
{
$widgetIdentifier = (string)($request->getQueryParams()['widget'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$widgets = [];
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
foreach ($dashboard->getWidgets() as $dashboardEntry) {
$widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry;
}
}
$dashboardWidget = $widgets[$widgetIdentifier] ?? null;
if ($dashboardWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
return new JsonResponse([
'status' => 'ok',
'widget' => $dashboardWidget->getTransferWidgetData()->jsonSerialize(),
]);
}
public function getWidgetSettings(ServerRequestInterface $request): ResponseInterface
{
$widgetIdentifier = (string)($request->getQueryParams()['widget'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$widgets = [];
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
foreach ($dashboard->getWidgets() as $dashboardEntry) {
$widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry;
}
}
$dashboardWidget = $widgets[$widgetIdentifier] ?? null;
if ($dashboardWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
$categories = [
new Category(
key: $dashboardWidget->getType(),
label: $this->getLanguageService()->sL($dashboardWidget->getTitle()),
description: $this->getLanguageService()->sL($dashboardWidget->getDescription()),
icon: $dashboardWidget->getIconIdentifier(),
settings: array_map(
fn(SettingDefinition $definition): EditableSetting => new EditableSetting(
definition: $this->resolveSettingLabels($definition),
value: $dashboardWidget->getSettings()->get($definition->key),
systemDefault: $definition->default,
typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(),
),
array_values(array_filter($dashboardWidget->getSettingsDefinitions(), fn(SettingDefinition $settingDefinition) => !$settingDefinition->readonly))
),
),
];
return new JsonResponse([
'status' => 'ok',
'categories' => json_encode($categories),
]);
}
public function updateWidgetSettings(ServerRequestInterface $request): ResponseInterface
{
// Check if identifier is available
$widgetIdentifier = trim((string)($request->getParsedBody()['widget'] ?? ''));
if ($widgetIdentifier === '') {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget is not available!',
]);
}
// Check for widget
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$targetDashboard = null;
$targetWidget = null;
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
if ($dashboard->getWidget($widgetIdentifier)) {
$targetDashboard = $dashboard;
$targetWidget = $dashboard->getWidget($widgetIdentifier);
break;
}
}
if ($targetWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
if ($targetDashboard === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard does not exist!',
]);
}
$rawSettings = $request->getParsedBody()['settings'] ?? [];
$widgetData = [];
foreach ($targetDashboard->getWidgets() as $widget) {
$widgetData[$widget->getIdentifier()] = $widget->getRawConfig();
if ($targetWidget->getIdentifier() === $widget->getIdentifier()) {
$currentSettings = $widget->getRawConfig()['settings'] ?? [];
$newSettings = $this->widgetSettingsFactory->createSettingsFromFormData($rawSettings, $widget->getSettingsDefinitions());
$defaultSettings = $this->widgetSettingsFactory->createSettings($widget->getType(), [], $widget->getSettingsDefinitions());
$diff = SettingsDiff::create(
$currentSettings,
$newSettings,
$defaultSettings,
);
if ($diff->changes === [] && $diff->deletions === []) {
return new JsonResponse([
'status' => 'info',
'message' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.settings.unchanged'),
]);
}
$widgetData[$widget->getIdentifier()]['settings'] = $diff->settings;
}
}
$this->dashboardRepository->updateWidgetConfig($targetDashboard, $widgetData);
// Fetch updated Dashboard
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($targetDashboard->getIdentifier());
$dashboardEntity->initializeWidgets($request);
$returnWidget = $dashboardEntity->getWidget($targetWidget->getIdentifier());
if ($returnWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
return new JsonResponse([
'status' => 'ok',
]);
}
public function addWidget(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['dashboard'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboard, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$widgetType = (string)($request->getParsedBody()['type'] ?? '');
if ($widgetType === '') {
throw new \InvalidArgumentException('Argument "widget" not set.', 1714987384);
}
$widgets = $dashboard->getWidgetConfig();
$widgetIdentifier = sha1($widgetType . '-' . time());
$widgets[$widgetIdentifier] = ['identifier' => $widgetType];
$this->dashboardRepository->updateWidgetConfig($dashboard, $widgets);
// Fetch updated Dashboard
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
$dashboard->initializeWidgets($request);
$widgets = [];
foreach ($dashboard->getWidgets() as $dashboardEntry) {
$widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry;
}
$dashboardWidget = $widgets[$widgetIdentifier] ?? null;
if ($dashboardWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget is not available!',
]);
}
return new JsonResponse([
'status' => 'ok',
'widget' => $dashboardWidget->getTransferWidgetData()->jsonSerialize(),
]);
}
public function removeWidget(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['dashboard'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboard, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$widgetIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$widgets = $dashboard->getWidgetConfig();
if ($widgetIdentifier === '' || !array_key_exists($widgetIdentifier, $widgets)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget is not available!',
]);
}
unset($widgets[$widgetIdentifier]);
$this->dashboardRepository->updateWidgetConfig($dashboard, $widgets);
return new JsonResponse([
'status' => 'ok',
]);
}
private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition
{
$languageService = $this->getLanguageService();
return new SettingDefinition(...[
...get_object_vars($definition),
'label' => $languageService->sL($definition->label),
'description' => $definition->description !== null ? $languageService->sL($definition->description) : null,
'enum' => array_map(static fn(string $label): string => $languageService->sL($label), $definition->enum),
]);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Dashboard\DashboardInitializationService;
/**
* @internal
*/
#[AsController]
readonly class DashboardController
{
public function __construct(
protected PageRenderer $pageRenderer,
protected DashboardInitializationService $dashboardInitializationService,
protected ModuleTemplateFactory $moduleTemplateFactory,
) {}
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
$this->dashboardInitializationService->initializeDashboards($request, $this->getBackendUser());
$view = $this->moduleTemplateFactory->create($request);
$this->preparePageRenderer();
$this->addFrontendResources();
$view->setTitle($this->getLanguageService()->translate('title', 'dashboard.module'));
$view->getDocHeaderComponent()->disable();
return $view->renderResponse('Dashboard/Main');
}
/**
* Adds CSS and JS files that are necessary for widgets to the page renderer
*/
protected function addFrontendResources(): void
{
$javaScriptRenderer = $this->pageRenderer->getJavaScriptRenderer();
foreach ($this->dashboardInitializationService->getJavaScriptModuleInstructions() as $instruction) {
$javaScriptRenderer->addJavaScriptModuleInstruction($instruction);
}
foreach ($this->dashboardInitializationService->getCssFiles() as $cssFile) {
$this->pageRenderer->addCssFile($cssFile);
}
foreach ($this->dashboardInitializationService->getJsFiles() as $jsFile) {
$this->pageRenderer->addJsFile($jsFile);
}
}
/**
* Add the CSS and JS of the dashboard module to the page renderer
*/
protected function preparePageRenderer(): void
{
$this->pageRenderer->loadJavaScriptModule('@typo3/dashboard/dashboard.js');
$this->pageRenderer->addCssFile('EXT:dashboard/Resources/Public/Css/dashboard.css');
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\Settings;
use TYPO3\CMS\Dashboard\Dto\Dashboard as TransferDashboard;
use TYPO3\CMS\Dashboard\Dto\WidgetConfiguration as TransferWidgetConfiguration;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
/**
* @internal
*/
class Dashboard
{
/**
* @var array<string,DashboardEntry>
*/
protected array $widgets = [];
protected ?object $widgetPositions = null;
/**
* @param array<string,array<string,string|array>> $widgetConfig
*/
public function __construct(
protected readonly string $identifier,
protected readonly string $title,
protected readonly array $widgetConfig,
protected readonly WidgetRegistry $widgetRegistry,
protected readonly WidgetSettingsFactory $widgetSettingsFactory,
protected readonly ContainerInterface $container,
) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getTitle(): string
{
return $this->getLanguageService()->sL($this->title) ?: $this->title;
}
public function getWidgetConfig(): array
{
return $this->widgetConfig;
}
/**
* @return array<string,DashboardEntry>
*/
public function getWidgets(): array
{
return $this->widgets;
}
public function getWidget(string $identifier): ?DashboardEntry
{
return $this->widgets[$identifier] ?? null;
}
public function getWidgetPositions(): object
{
return $this->widgetPositions ?? new \stdClass();
}
/**
* This will return a list of all widgets of the current dashboard object. It will only include available
* widgets and will add the initialized object of the widget itself
*/
public function initializeWidgets(ServerRequestInterface $request): void
{
$availableWidgets = $this->widgetRegistry->getAvailableWidgets();
$this->widgetPositions = new \stdClass();
foreach ($this->widgetConfig as $hash => $widgetConfig) {
$widgetConfigIdentifier = $widgetConfig['identifier'] ?? '';
if ($widgetConfigIdentifier !== '' && array_key_exists($widgetConfigIdentifier, $availableWidgets)) {
// Widget (Renderer) Instance
$widgetRenderer = $this->widgetRegistry->getAvailableWidget($request, $widgetConfigIdentifier);
// Dashboard Entry with Widget Context
$this->widgets[$hash] = new DashboardEntry(
context: new WidgetContext(
identifier: $hash,
rawData: $widgetConfig,
configuration: $availableWidgets[$widgetConfigIdentifier],
settings: $widgetRenderer instanceof WidgetRendererInterface ? $this->widgetSettingsFactory->createSettings(
$widgetConfigIdentifier,
$widgetConfig['settings'] ?? [],
$widgetRenderer->getSettingsDefinitions(),
) : new Settings([]),
request: $request,
),
renderer: $widgetRenderer,
);
// Widget Positions
$positions = $widgetConfig['positions'] ?? [];
foreach ($positions as $columnCount => $position) {
if (!isset($position['height']) || !isset($position['width']) || !isset($position['x']) || !isset($position['y'])) {
continue;
}
if (!isset($this->widgetPositions->{$columnCount})) {
$this->widgetPositions->{$columnCount} = [];
}
$this->widgetPositions->{$columnCount}[] = [
'identifier' => $hash,
'height' => (int)$position['height'],
'width' => (int)$position['width'],
'x' => (int)$position['x'],
'y' => (int)$position['y'],
];
}
}
}
foreach (array_keys(get_object_vars($this->widgetPositions)) as $columnCount) {
usort(
$this->widgetPositions->{$columnCount},
static fn(array $a, array $b): int => $a['y'] !== $b['y'] ? $a['y'] - $b['y'] : $a['x'] - $b['x']
);
}
}
public function getTransferData(): TransferDashboard
{
return new TransferDashboard(
identifier: $this->getIdentifier(),
title: $this->getTitle(),
widgets: array_values(array_map(fn(DashboardEntry $entry): TransferWidgetConfiguration => $entry->getTransferWidgetConfiguration(), $this->getWidgets())),
widgetPositions: $this->getWidgetPositions(),
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+172
View File
@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Dashboard\Dto\WidgetConfiguration as TransferWidgetConfiguration;
use TYPO3\CMS\Dashboard\Dto\WidgetData as TransferWidgetData;
use TYPO3\CMS\Dashboard\Widgets\EventDataInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetResult;
/**
* Dashboard entry representing a widget instance within a dashboard.
*
* This class encapsulates a dashboard widget instance, providing access to its context,
* configuration, settings, and rendering capabilities. It serves as a bridge between
* the dashboard system and individual widget implementations, handling both legacy
* WidgetInterface and new WidgetRendererInterface widgets.
*
* Each dashboard entry maintains its own widget context with instance-specific settings
* and provides methods for rendering and configuration management.
*
* @internal
*/
final readonly class DashboardEntry
{
public function __construct(
private WidgetContext $context,
private WidgetRendererInterface|WidgetInterface $renderer,
) {}
public function getIdentifier(): string
{
return $this->context->identifier;
}
public function getType(): string
{
return $this->context->configuration->getIdentifier();
}
public function getTitle(): string
{
return $this->context->configuration->getTitle();
}
public function getDescription(): string
{
return $this->context->configuration->getDescription();
}
public function getIconIdentifier(): string
{
return $this->context->configuration->getIconIdentifier();
}
public function getHeight(): string
{
return $this->context->configuration->getHeight();
}
public function getWidth(): string
{
return $this->context->configuration->getWidth();
}
public function getSettings(): SettingsInterface
{
return $this->context->settings;
}
public function getRequest(): ServerRequestInterface
{
return $this->context->request;
}
public function getEventData(): array
{
return ($this->renderer instanceof EventDataInterface) ? $this->renderer->getEventData() : [];
}
public function getTransferWidgetConfiguration(): TransferWidgetConfiguration
{
return new TransferWidgetConfiguration(
identifier: $this->getIdentifier(),
type: $this->getType(),
height: $this->getHeight(),
width: $this->getWidth(),
);
}
public function getTransferWidgetData(): TransferWidgetData
{
$result = $this->render();
return new TransferWidgetData(
identifier: $this->getIdentifier(),
type: $this->getType(),
height: $this->getHeight(),
width: $this->getWidth(),
label: $result->label ?? $this->getLanguageService()->sL($this->getTitle()),
content: $result->content,
eventdata: $this->getEventData(),
refreshable: $result->refreshable,
configurable: (
$this->renderer instanceof WidgetRendererInterface
&& array_filter($this->renderer->getSettingsDefinitions(), fn($definition) => !$definition->readonly) !== []
)
);
}
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array
{
if ($this->renderer instanceof WidgetRendererInterface) {
return $this->renderer->getSettingsDefinitions();
}
return [];
}
public function getRawConfig(): array
{
return $this->context->rawData;
}
private function render(): WidgetResult
{
try {
if ($this->renderer instanceof WidgetRendererInterface) {
return $this->renderer->renderWidget($this->context);
}
// Map legacy WidgetInterface to "new" WidgetResult
return new WidgetResult(
content: $this->renderer->renderWidgetContent(),
refreshable: $this->renderer->getOptions()['refreshAvailable'] ?? false,
);
} catch (\Exception) {
return new WidgetResult(
content: sprintf('<div class="widget-content-main">%s</div>', $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.error')),
refreshable: true,
);
}
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Repository\DashboardRepository;
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
use TYPO3\CMS\Dashboard\Widgets\AdditionalJavaScriptInterface;
use TYPO3\CMS\Dashboard\Widgets\JavaScriptInterface;
/**
* @internal
*/
class DashboardInitializationService
{
protected const MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER = 'dashboard/current_dashboard/';
/**
* @var list<JavaScriptModuleInstruction>
*/
protected array $javaScriptModuleInstructions = [];
private array $jsFiles = [];
private array $cssFiles = [];
public function __construct(
private readonly DashboardRepository $dashboardRepository,
private readonly DashboardPresetRegistry $dashboardPresetRegistry,
private readonly WidgetRegistry $widgetRegistry,
) {}
public function initializeDashboards(ServerRequestInterface $request, BackendUserAuthentication $user): void
{
$this->defineCurrentDashboard($user);
$this->defineResourcesOfWidgets();
}
protected function defineCurrentDashboard(BackendUserAuthentication $user): Dashboard
{
$currentDashboard = $this->dashboardRepository->getDashboardByIdentifier($this->loadCurrentDashboard($user));
if ($currentDashboard === null) {
$dashboards = $this->getDashboardsForUser($user);
/** @var Dashboard $currentDashboard */
$currentDashboard = reset($dashboards);
$this->saveCurrentDashboard($user, $currentDashboard->getIdentifier());
}
return $currentDashboard;
}
protected function createDefaultDashboards(BackendUserAuthentication $user): array
{
$dashboardsForUser = [];
$userConfig = $user->getTSConfig();
$dashboardsToCreate = GeneralUtility::trimExplode(
',',
$userConfig['options.']['dashboard.']['dashboardPresetsForNewUsers'] ?? 'default'
);
foreach ($this->dashboardPresetRegistry->getDashboardPresets() as $dashboardPreset) {
if (in_array($dashboardPreset->getIdentifier(), $dashboardsToCreate, true)) {
$dashboard = $this->dashboardRepository->create(
$dashboardPreset,
(int)$user->user['uid']
);
if ($dashboard === null) {
continue;
}
$dashboardsForUser[$dashboard->getIdentifier()] = $dashboard;
}
}
return $dashboardsForUser;
}
/**
* @return Dashboard[]
*/
public function getDashboardsForUser(BackendUserAuthentication $user): array
{
$dashboards = [];
foreach ($this->dashboardRepository->getDashboardsForUser((int)$user->user['uid']) as $dashboard) {
$dashboards[$dashboard->getIdentifier()] = $dashboard;
}
if ($dashboards === []) {
$dashboards = $this->createDefaultDashboards($user);
}
return $dashboards;
}
protected function defineResourcesOfWidgets(): void
{
foreach ($this->widgetRegistry->getAvailableWidgets() as $widget) {
$concreteInstance = GeneralUtility::makeInstance($widget->getServiceName());
if ($concreteInstance instanceof JavaScriptInterface) {
$this->defineJavaScriptInstructions($concreteInstance);
}
if ($concreteInstance instanceof AdditionalCssInterface) {
$this->defineCssFiles($concreteInstance);
}
if ($concreteInstance instanceof AdditionalJavaScriptInterface) {
$this->defineJsFiles($concreteInstance);
}
}
}
protected function defineJavaScriptInstructions(JavaScriptInterface $widgetInstance): void
{
foreach ($widgetInstance->getJavaScriptModuleInstructions() as $instruction) {
$this->javaScriptModuleInstructions[] = $instruction;
}
}
/**
* Define the correct path of the JS files of a widget and add them to the list of JS files that needs to be
* included
*/
protected function defineJsFiles(AdditionalJavaScriptInterface $widgetInstance): void
{
foreach ($widgetInstance->getJsFiles() as $jsFile) {
$this->jsFiles[$jsFile] = $jsFile;
}
}
/**
* Define the correct path of the CSS files of a widget and add them to the list of CSS files that needs to be
* included
*/
protected function defineCssFiles(AdditionalCssInterface $widgetInstance): void
{
foreach ($widgetInstance->getCssFiles() as $cssFile) {
$this->cssFiles[$cssFile] = $cssFile;
}
}
protected function loadCurrentDashboard(BackendUserAuthentication $user): string
{
return $user->getModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER) ?? '';
}
protected function saveCurrentDashboard(BackendUserAuthentication $user, string $identifier): void
{
$user->pushModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER, $identifier);
}
/**
* @return list<JavaScriptModuleInstruction>
*/
public function getJavaScriptModuleInstructions(): array
{
return $this->javaScriptModuleInstructions;
}
public function getJsFiles(): array
{
return $this->jsFiles;
}
public function getCssFiles(): array
{
return $this->cssFiles;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* @internal
*/
readonly class DashboardPreset implements \JsonSerializable
{
/**
* @param list<array{identifier: string, settings?: array<string, mixed>}> $defaultWidgets
*/
public function __construct(
protected string $identifier,
protected string $title,
protected string $description,
protected string $iconIdentifier = 'content-dashboard',
protected array $defaultWidgets = [],
protected bool $showInWizard = true
) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getIconIdentifier(): string
{
return $this->iconIdentifier;
}
public function getTitle(): string
{
return $this->getLanguageService()->sL($this->title) ?: $this->title;
}
public function getDescription(): string
{
return $this->getLanguageService()->sL($this->description) ?: $this->description;
}
/**
* @return list<array{identifier: string, settings?: array<string, mixed>}>
*/
public function getDefaultWidgets(): array
{
return $this->defaultWidgets;
}
public function isShowInWizard(): bool
{
return $this->showInWizard;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
public function jsonSerialize(): array
{
return [
'identifier' => $this->getIdentifier(),
'title' => $this->getTitle(),
'description' => $this->getDescription(),
'icon' => $this->getIconIdentifier(),
'widgets' => $this->getDefaultWidgets(),
'showInWizard' => $this->isShowInWizard(),
];
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
/**
* @internal
*/
class DashboardPresetRegistry
{
/**
* @var DashboardPreset[]
*/
private array $dashboardPresets = [];
public function getDashboardPresets(): array
{
if (empty($this->dashboardPresets)) {
$fallbackDashboardPreset = new DashboardPreset(
'dashboardPreset-fallback',
'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default',
'',
'content-dashboard',
[],
false
);
return [
'dashboardPreset-fallback' => $fallbackDashboardPreset,
];
}
return $this->dashboardPresets;
}
public function registerDashboardPreset(DashboardPreset $dashboardPreset): void
{
$this->dashboardPresets[$dashboardPreset->getIdentifier()] = $dashboardPreset;
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\WidgetRegistry;
use TYPO3\CMS\Dashboard\Widgets\AdminOnlyWidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration;
/**
* @internal
*/
final readonly class DashboardWidgetPass implements CompilerPassInterface
{
public function __construct(private string $tagName) {}
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition(WidgetRegistry::class)) {
return;
}
$widgetRegistryDefinition = $container->findDefinition(WidgetRegistry::class);
foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
$definition = $container->findDefinition($serviceName);
$definition->setPublic(true);
foreach ($tags as $attributes) {
$identifier = $attributes['identifier'] ?? $serviceName;
$attributes['identifier'] = $identifier;
$attributes['serviceName'] = $serviceName;
$attributes['adminOnly'] = is_a($definition->getClass(), AdminOnlyWidgetInterface::class, true);
$attributes = $this->convertAttributes($attributes);
$configurationServiceName = $this->registerWidgetConfigurationService(
$container,
$identifier,
$attributes
);
$definition->setArgument('$configuration', new Reference($configurationServiceName));
$widgetRegistryDefinition->addMethodCall('registerWidget', [$identifier . 'WidgetConfiguration']);
}
}
}
private function convertAttributes(array $attributes): array
{
$attributes = array_merge([
'iconIdentifier' => 'content-dashboard',
'height' => 'small',
'width' => 'small',
], $attributes);
if (isset($attributes['groupNames'])) {
$attributes['groupNames'] = GeneralUtility::trimExplode(',', $attributes['groupNames'], true);
} else {
$attributes['groupNames'] = [];
}
return $attributes;
}
private function registerWidgetConfigurationService(
ContainerBuilder $container,
string $widgetIdentifier,
array $arguments
): string {
$serviceName = $widgetIdentifier . 'WidgetConfiguration';
$definition = new Definition(
WidgetConfiguration::class,
$this->adjustArgumentsForDi($arguments)
);
$definition->setPublic(true);
$container->addDefinitions([$serviceName => $definition]);
return $serviceName;
}
private function adjustArgumentsForDi(array $arguments): array
{
foreach ($arguments as $key => $value) {
$arguments['$' . $key] = $value;
unset($arguments[$key]);
}
return $arguments;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Dto;
/**
* @internal
*/
final readonly class Dashboard implements \JsonSerializable
{
public function __construct(
private string $identifier,
private string $title,
private array $widgets,
private object $widgetPositions,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'title' => $this->title,
'widgets' => $this->widgets,
'widgetPositions' => $this->widgetPositions,
];
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Dto;
/**
* @internal
*/
final readonly class WidgetConfiguration implements \JsonSerializable
{
public function __construct(
private string $identifier,
private string $type,
private string $height,
private string $width,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'type' => $this->type,
'height' => $this->height,
'width' => $this->width,
];
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Dto;
/**
* @internal
*/
final readonly class WidgetData implements \JsonSerializable
{
public function __construct(
private string $identifier,
private string $type,
private string $height,
private string $width,
private string $label,
private string $content,
private array $eventdata,
private bool $refreshable,
private bool $configurable,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'type' => $this->type,
'height' => $this->height,
'width' => $this->width,
'label' => $this->label,
'content' => $this->content,
'eventdata' => $this->eventdata,
'refreshable' => $this->refreshable,
'configurable' => $this->configurable,
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Dto;
/**
* @internal
*/
final readonly class WidgetPosition implements \JsonSerializable
{
public function __construct(
private string $identifier,
private int $height,
private int $width,
private int $y,
private int $x,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'height' => $this->height,
'width' => $this->width,
'y' => $this->y,
'x' => $this->x,
];
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\EventListener;
use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\PageRenderer;
/**
* Adds custom CSS needed for EXT:dashboard in the "outer" backend scaffold
*/
final readonly class AfterBackendPageRenderEventListener
{
public function __construct(private PageRenderer $pageRenderer) {}
#[AsEventListener(event: AfterBackendPageRenderEvent::class)]
public function __invoke(): void
{
$this->pageRenderer->addCssFile('EXT:dashboard/Resources/Public/Css/Modal/style.css');
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Exception;
use TYPO3\CMS\Core\Exception;
class InvalidRssFeedException extends Exception {}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Factory;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingDefinitionValidation;
use TYPO3\CMS\Core\Settings\SettingsFactory;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Core\Settings\SettingsProvider;
/**
* Factory for creating widget settings instances.
*
* This factory creates Settings objects for dashboard widgets, combining default
* values from setting definitions with instance-specific values. It handles
* validation of setting definitions and provides utilities for creating settings
* from form data submitted through the widget configuration interface.
*
* The factory supports creating settings with various options:
* - Respecting readonly settings
* - Omitting default values when needed
* - Processing form data for widget configuration updates
*/
#[Autoconfigure(public: true)]
readonly class WidgetSettingsFactory
{
public function __construct(
protected SettingsFactory $settingsFactory,
protected SettingDefinitionValidation $settingDefinitionValidation,
) {}
/**
* @param SettingDefinition[] $definitions
*/
public function createSettings(
string $name,
array $settings,
array $definitions,
bool $respectReadonly = false,
bool $omitDefaults = false,
): SettingsInterface {
$defaultSettings = [];
foreach ($definitions as $definition) {
$this->settingDefinitionValidation->validate($definition);
if (!$omitDefaults) {
$defaultSettings[$definition->key] = $definition->default;
}
}
return $this->settingsFactory->resolveSettings(
new SettingsProvider($name, $defaultSettings, $definitions),
new SettingsProvider($name . ':instance', $settings, []),
);
}
public function createSettingsFromFormData(array $settings, array $definitions): SettingsInterface
{
return $this->settingsFactory->createSettingsFromFormData($settings, $definitions);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Persistence;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Fill the "cruser_id" for new dashboards.
*
* @internal this is a TYPO3-internal specific hook implementation and not part of TYPO3's Public API
*/
class DashboardCreationEnricher
{
/**
* @param array $incomingFieldArray
* @param string $table
* @param string $id
*/
public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table, $id, DataHandler $dataHandler)
{
// Not within be_dashboards
if ($table !== 'be_dashboards') {
return;
}
// Existing record, nothing to change
if (MathUtility::canBeInterpretedAsInteger($id)) {
return;
}
if (isset($incomingFieldArray['cruser_id'])) {
return;
}
$incomingFieldArray['cruser_id'] = $dataHandler->BE_USER->user['uid'] ?? 0;
}
}
+195
View File
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Repository;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Dashboard\Dashboard;
use TYPO3\CMS\Dashboard\DashboardPreset;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\WidgetRegistry;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
/**
* @internal
*/
class DashboardRepository
{
private const string TABLE = 'be_dashboards';
/**
* @var string[]
*/
protected array $allowedFields = ['title'];
/**
* @var list<WidgetRendererInterface|WidgetInterface>
*/
protected array $widgets = [];
public function __construct(
protected readonly ConnectionPool $connectionPool,
protected readonly WidgetRegistry $widgetRegistry,
protected readonly WidgetSettingsFactory $widgetSettingsFactory,
protected readonly ContainerInterface $container
) {}
/**
* @return Dashboard[]
*/
public function getDashboardsForUser(int $userId): array
{
$queryBuilder = $this->getQueryBuilder();
$rows = $queryBuilder
->select('*')
->from(self::TABLE)
->where(
$queryBuilder->expr()->eq('cruser_id', $queryBuilder->createNamedParameter($userId))
)
->executeQuery()
->fetchAllAssociative();
$results = [];
foreach ($rows as $row) {
$results[] = $this->createFromRow($row);
}
return $results;
}
public function create(DashboardPreset $dashboardPreset, int $userId, string $title = ''): ?Dashboard
{
$widgets = [];
$title = $title ?: $dashboardPreset->getTitle();
foreach ($dashboardPreset->getDefaultWidgets() as $defaultWidget) {
$hash = sha1(StringUtility::getUniqueId('widget_') . '-' . $defaultWidget['identifier']);
$widgets[$hash] = $defaultWidget;
}
$identifier = sha1($dashboardPreset->getIdentifier() . '-' . time());
$this->getQueryBuilder()
->insert(self::TABLE)
->values([
'identifier' => $identifier,
'title' => $title,
'tstamp' => time(),
'crdate' => time(),
'cruser_id' => $userId,
'widgets' => json_encode($widgets),
])
->executeStatement();
return $this->getDashboardByIdentifier($identifier);
}
/**
* @return int|null
*/
public function updateDashboardSettings(string $identifier, array $values)
{
$checkedValues = $this->checkAllowedFields($values);
if (empty($checkedValues)) {
return null;
}
$queryBuilder = $this->getQueryBuilder();
$queryBuilder->update(self::TABLE)
->where(
$queryBuilder->expr()->eq(
'identifier',
$queryBuilder->createNamedParameter($identifier)
)
);
foreach ($checkedValues as $field => $value) {
$queryBuilder->set($field, $value);
}
return $queryBuilder->executeStatement();
}
protected function checkAllowedFields(array $values): array
{
$allowedFields = [];
foreach ($values as $field => $value) {
if (!empty($value) && in_array((string)$field, $this->allowedFields, true)) {
$allowedFields[$field] = $value;
}
}
return $allowedFields;
}
public function getDashboardByIdentifier(string $identifier): ?Dashboard
{
$queryBuilder = $this->getQueryBuilder();
$row = $queryBuilder
->select('*')
->from(self::TABLE)
->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($identifier)))
->executeQuery()
->fetchAllAssociative();
if (count($row)) {
return $this->createFromRow($row[0]);
}
return null;
}
/**
* @param array<array<string, mixed>> $widgets
*/
public function updateWidgetConfig(Dashboard $dashboard, array $widgets): void
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->update(self::TABLE)
->set('widgets', json_encode($widgets))
->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($dashboard->getIdentifier())))
->executeStatement();
}
public function delete(Dashboard $dashboard): void
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->update(self::TABLE)
->set('deleted', 1)
->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($dashboard->getIdentifier())))
->executeStatement();
}
protected function createFromRow(array $row): Dashboard
{
return GeneralUtility::makeInstance(
Dashboard::class,
$row['identifier'] ?? '',
$row['title'] ?? '',
json_decode((string)($row['widgets'] ?? ''), true) ?? [],
$this->widgetRegistry,
$this->widgetSettingsFactory,
$this->container
);
}
protected function getQueryBuilder(): QueryBuilder
{
return $this->connectionPool->getQueryBuilderForTable(self::TABLE);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Package\AbstractServiceProvider;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
class ServiceProvider extends AbstractServiceProvider
{
private const string CACHE_IDENTIFIER_PREFIX = 'Dashboard_';
protected static function getPackagePath(): string
{
return __DIR__ . '/../';
}
protected static function getPackageName(): string
{
return 'typo3/cms-dashboard';
}
public function getFactories(): array
{
return [
'dashboard.presets' => self::getDashboardPresets(...),
'dashboard.widgetGroups' => self::getWidgetGroups(...),
'dashboard.widgets' => self::getWidgets(...),
'dashboard.configuration.warmer' => self::getConfigurationWarmer(...),
];
}
public function getExtensions(): array
{
return [
DashboardPresetRegistry::class => self::configureDashboardPresetRegistry(...),
ListenerProvider::class => self::addEventListeners(...),
WidgetGroupRegistry::class => self::configureWidgetGroupRegistry(...),
'dashboard.presets' => self::configureDashboardPresets(...),
'dashboard.widgetGroups' => self::configureWidgetGroups(...),
'dashboard.widgets' => self::configureWidgets(...),
] + parent::getExtensions();
}
public static function getDashboardPresets(ContainerInterface $container): \ArrayObject
{
return new \ArrayObject();
}
public static function getWidgetGroups(ContainerInterface $container): \ArrayObject
{
return new \ArrayObject();
}
public static function getWidgets(ContainerInterface $container): \ArrayObject
{
return new \ArrayObject();
}
public static function configureDashboardPresetRegistry(
ContainerInterface $container,
?DashboardPresetRegistry $dashboardPresetRegistry = null
): DashboardPresetRegistry {
$dashboardPresetRegistry = $dashboardPresetRegistry ?? self::new($container, DashboardPresetRegistry::class);
$cache = $container->get('cache.core');
$cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'Presets')->toString();
if (!$dashboardPresetsFromPackages = $cache->require($cacheIdentifier)) {
$dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy();
$cache->set($cacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';');
}
foreach ($dashboardPresetsFromPackages as $identifier => $options) {
// Compatibility layer for presets using the widget identifier only
$defaultWidgets = array_map(
static fn(string|array $defaultWidget): array => is_string($defaultWidget) ? ['identifier' => $defaultWidget] : $defaultWidget,
$options['defaultWidgets'] ?? []
);
$preset = new DashboardPreset(
$identifier,
$options['title'],
$options['description'],
$options['iconIdentifier'],
$defaultWidgets,
$options['showInWizard']
);
$dashboardPresetRegistry->registerDashboardPreset($preset);
}
return $dashboardPresetRegistry;
}
public static function configureWidgetGroupRegistry(
ContainerInterface $container,
?WidgetGroupRegistry $widgetGroupRegistry = null
): WidgetGroupRegistry {
$widgetGroupRegistry = $widgetGroupRegistry ?? self::new($container, WidgetGroupRegistry::class);
$cache = $container->get('cache.core');
$cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'WidgetGroups')->toString();
if (!$widgetGroupsFromPackages = $cache->require($cacheIdentifier)) {
$widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy();
$cache->set($cacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';');
}
foreach ($widgetGroupsFromPackages as $identifier => $options) {
$group = new WidgetGroup(
$identifier,
$options['title']
);
$widgetGroupRegistry->registerWidgetGroup($group);
}
return $widgetGroupRegistry;
}
public static function configureDashboardPresets(ContainerInterface $container, \ArrayObject $presets): \ArrayObject
{
$paths = self::getPathsOfInstalledPackages();
foreach ($paths as $pathOfPackage) {
$dashboardPresetsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardPresets.php';
if (file_exists($dashboardPresetsFileNameForPackage)) {
$definedPresetsInPackage = self::requireFile($dashboardPresetsFileNameForPackage);
if (is_array($definedPresetsInPackage)) {
$presets->exchangeArray(array_merge($presets->getArrayCopy(), $definedPresetsInPackage));
}
}
}
return $presets;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
*/
public static function configureWidgetGroups(ContainerInterface $container, \ArrayObject $widgetGroups, ?string $path = null): \ArrayObject
{
$paths = self::getPathsOfInstalledPackages();
foreach ($paths as $pathOfPackage) {
$widgetGroupsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardWidgetGroups.php';
if (file_exists($widgetGroupsFileNameForPackage)) {
$definedGroupsInPackage = self::requireFile($widgetGroupsFileNameForPackage);
if (is_array($definedGroupsInPackage)) {
$widgetGroups->exchangeArray(array_merge($widgetGroups->getArrayCopy(), $definedGroupsInPackage));
}
}
}
return $widgetGroups;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
*/
public static function configureWidgets(ContainerInterface $container, \ArrayObject $widgets, ?string $path = null): \ArrayObject
{
$paths = self::getPathsOfInstalledPackages();
foreach ($paths as $pathOfPackage) {
$widgetsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardWidgets.php';
if (file_exists($widgetsFileNameForPackage)) {
$definedWidgetsInPackage = self::requireFile($widgetsFileNameForPackage);
if (is_array($definedWidgetsInPackage)) {
$widgets->exchangeArray(array_merge($widgets->getArrayCopy(), $definedWidgetsInPackage));
}
}
}
return $widgets;
}
protected static function getPathsOfInstalledPackages(): array
{
$paths = [];
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
foreach ($packageManager->getActivePackages() as $package) {
$paths[] = $package->getPackagePath();
}
return $paths;
}
public static function getConfigurationWarmer(ContainerInterface $container): \Closure
{
$cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class);
$presetsCacheIdentifier = $cacheIdentifier->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'Presets')->toString();
$widgetGroupsCacheIdentifier = $cacheIdentifier->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'WidgetGroups')->toString();
return static function (CacheWarmupEvent $event) use ($container, $presetsCacheIdentifier, $widgetGroupsCacheIdentifier) {
if ($event->hasGroup('system')) {
$cache = $container->get('cache.core');
$dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy();
$cache->set($presetsCacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';');
$widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy();
$cache->set($widgetGroupsCacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';');
}
};
}
public static function addEventListeners(ContainerInterface $container, ListenerProvider $listenerProvider): ListenerProvider
{
$listenerProvider->addListener(CacheWarmupEvent::class, 'dashboard.configuration.warmer');
return $listenerProvider;
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\ViewHelpers\Widget;
use TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface;
use TYPO3\CMS\Dashboard\Widgets\ElementAttributesInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* @internal
*
* Renders a dashboard button
*
* Examples
* ========
*
* ::
*
* <dashboard:widget.button button="{button}" class="widget-cta">
* {f:translate(id: button.title, default: button.title)}
* </dashboard:widget.button>
*/
final class ButtonViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('button', ButtonProviderInterface::class, 'Dashboard widget button', true);
}
public function render(): string
{
$button = $this->arguments['button'];
$this->tag->addAttribute('href', $button->getLink() ?: '#');
$target = $button->getTarget();
if ($target !== '') {
$this->tag->addAttribute('target', $target);
if ($target === '_blank') {
$this->tag->addAttribute('rel', 'noreferrer');
}
}
if ($button instanceof ElementAttributesInterface) {
$this->tag->addAttributes($button->getElementAttributes());
}
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
/**
* Provides API for widgets.
*/
class WidgetApi
{
/**
* Provides default colors to use for charts.
*
* @return array{0:string, 1:string, 2:string, 3:string, 4:string} Hex codes of default colors.
*/
public static function getDefaultChartColors(): array
{
return [
'#ff8700',
'#a4276a',
'#1a568f',
'#4c7e3a',
'#69bbb5',
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* @internal
*/
readonly class WidgetGroup
{
public function __construct(protected string $identifier, protected string $title) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getTitle(): string
{
return $this->getLanguageService()->sL($this->title) ?: $this->title;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* @internal
*/
readonly class WidgetGroupInitializationService
{
public function __construct(
private WidgetGroupRegistry $widgetGroupRegistry,
private WidgetRegistry $widgetRegistry,
) {}
/**
* Define the different groups of widgets as shown in the modal when adding a widget to the current dashboard
*/
public function buildWidgetGroupsConfiguration(): array
{
$groupConfigurations = [];
foreach ($this->widgetGroupRegistry->getWidgetGroups() as $widgetGroup) {
$widgets = [];
$widgetGroupIdentifier = $widgetGroup->getIdentifier();
$widgetsForGroup = $this->widgetRegistry->getAvailableWidgetsForWidgetGroup($widgetGroupIdentifier);
foreach ($widgetsForGroup as $widgetConfiguration) {
$widgetIdentifier = $widgetConfiguration->getIdentifier();
$widgets[] = [
'identifier' => $widgetIdentifier,
'icon' => $widgetConfiguration->getIconIdentifier(),
'label' => $this->getLanguageService()->sL($widgetConfiguration->getTitle()),
'description' => $this->getLanguageService()->sL($widgetConfiguration->getDescription()),
'requestType' => 'event',
'event' => 'typo3:dashboard:widget:add',
];
}
if ($widgets === []) {
continue;
}
$groupConfigurations[$widgetGroupIdentifier] = [
'identifier' => $widgetGroupIdentifier,
'label' => $widgetGroup->getTitle(),
'items' => $widgets,
];
}
return $groupConfigurations;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
/**
* @internal
*/
class WidgetGroupRegistry
{
/**
* @var WidgetGroup[]
*/
private array $widgetGroups = [];
public function getWidgetGroups(): array
{
return $this->widgetGroups;
}
public function registerWidgetGroup(WidgetGroup $widgetGroup): void
{
$this->widgetGroups[$widgetGroup->getIdentifier()] = $widgetGroup;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\Widgets\RequestAwareWidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
/**
* @internal
*/
#[Autoconfigure(public: true)]
class WidgetRegistry
{
/**
* @var array<string,WidgetConfigurationInterface>
*/
private $widgets = [];
/**
* @var array<string, WidgetConfigurationInterface[]>
*/
private $widgetsPerWidgetGroup = [];
public function __construct(
protected readonly ContainerInterface $container,
protected readonly WidgetSettingsFactory $widgetSettingsFactory,
) {}
/**
* @return WidgetConfigurationInterface[]
*/
public function getAvailableWidgets(): array
{
return $this->checkPermissionOfWidgets($this->widgets);
}
/**
* @return WidgetConfigurationInterface[]
*/
public function getAllWidgets(): array
{
return $this->widgets;
}
/**
* @throws \InvalidArgumentException If requested identifier does not exist.
*/
public function getAvailableWidget(ServerRequestInterface $request, string $identifier): WidgetRendererInterface|WidgetInterface
{
if (array_key_exists($identifier, $this->getAvailableWidgets())) {
$widget = $this->container->get($this->widgets[$identifier]->getServiceName());
if ($widget instanceof RequestAwareWidgetInterface) {
$widget->setRequest($request);
}
return $widget;
}
throw new \InvalidArgumentException('Requested widget "' . $identifier . '" does not exist.', 1584777201);
}
/**
* @return WidgetConfigurationInterface[]
*/
public function getAvailableWidgetsForWidgetGroup(string $widgetGroupIdentifier): array
{
if (!array_key_exists($widgetGroupIdentifier, $this->widgetsPerWidgetGroup)) {
return [];
}
return $this->checkPermissionOfWidgets($this->widgetsPerWidgetGroup[$widgetGroupIdentifier]);
}
public function registerWidget(string $serviceName): void
{
$widgetConfiguration = $this->container->get($serviceName);
$this->widgets[$widgetConfiguration->getIdentifier()] = $widgetConfiguration;
foreach ($widgetConfiguration->getGroupNames() as $groupIdentifier) {
$this->widgetsPerWidgetGroup = ArrayUtility::setValueByPath(
$this->widgetsPerWidgetGroup,
$groupIdentifier . '/' . $widgetConfiguration->getIdentifier(),
$widgetConfiguration
);
}
}
/**
* @param WidgetConfigurationInterface[] $widgets
* @return WidgetConfigurationInterface[]
*/
protected function checkPermissionOfWidgets(array $widgets): array
{
return array_filter($widgets, function ($widget, $identifier) {
return $this->getBackendUser()->check('available_widgets', $identifier)
&& (!$widget instanceof WidgetConfiguration || !$widget->isAdminOnly() || $this->getBackendUser()->isAdmin());
}, ARRAY_FILTER_USE_BOTH);
}
public function widgetItemsProcFunc(array &$parameters): void
{
foreach ($this->widgets as $widget) {
if ($widget instanceof WidgetConfiguration && $widget->isAdminOnly()) {
continue;
}
$parameters['items'][] = [
'label' => $widget->getTitle(),
'value' => $widget->getIdentifier(),
'icon' => $widget->getIconIdentifier(),
'description' => $widget->getDescription(),
];
}
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case a widget should provide additional CSS files, the widget must implement this interface.
*/
interface AdditionalCssInterface
{
/**
* This method returns an array with paths to required CSS files.
* e.g. ['EXT:myext/Resources/Public/Css/my_widget.css']
*/
public function getCssFiles(): array;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case a widget should provide additional JavaScript files, the widget must implement this interface.
*/
interface AdditionalJavaScriptInterface
{
/**
* This method returns an array with paths to required JS files.
* e.g. ['EXT:myext/Resources/Public/JavaScript/my_widget.js']
*/
public function getJsFiles(): array;
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* This interface should be used to describe a widget as restricted => it will not be able to assign to a user group
*/
interface AdminOnlyWidgetInterface {}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
/**
* Concrete Bar Chart widget implementation
*
* Shows a widget with a bar chart. The data for this chart will be provided by the data provider you will set.
* You can add a button to the widget by defining a button provider.
*
* There are no options available for this widget
*
* @see ChartDataProviderInterface
* @see ButtonProviderInterface
*/
class BarChartWidget implements WidgetInterface, RequestAwareWidgetInterface, EventDataInterface, AdditionalCssInterface, JavaScriptInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ChartDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'button' => $this->buttonProvider,
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/ChartWidget');
}
public function getEventData(): array
{
return [
'graphConfig' => [
'type' => 'bar',
'options' => [
'maintainAspectRatio' => false,
'plugins' => [
'legend' => [
'display' => false,
],
],
'scales' => [
'y' => [
'ticks' => [
'beginAtZero' => true,
],
],
'x' => [
'ticks' => [
'maxTicksLimit' => 15,
],
],
],
],
'data' => $this->dataProvider->getChartData(),
],
];
}
public function getCssFiles(): array
{
return [];
}
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create('@typo3/dashboard/contrib/chartjs.js'),
JavaScriptModuleInstruction::create('@typo3/dashboard/chart-initializer.js'),
];
}
public function getOptions(): array
{
return $this->options;
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use TYPO3\CMS\Backend\Backend\Bookmark\BookmarkService;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsInterface;
/**
* Widget to display user bookmarks on the dashboard.
*
* The widget renders a custom element that reads bookmark data
* from the central BookmarkStore in the top frame.
*/
final readonly class BookmarksWidget implements WidgetRendererInterface, JavaScriptInterface
{
public function __construct(
private WidgetConfigurationInterface $configuration,
private BackendViewFactory $backendViewFactory,
private BookmarkService $bookmarkService,
/** @var array{limit?: int, group?: string} */
private array $options = [],
) {}
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'group',
type: 'string',
default: $this->options['group'] ?? '',
label: 'dashboard.widget_bookmarks:widget.bookmarks.setting.groups.label',
description: 'dashboard.widget_bookmarks:widget.bookmarks.setting.groups.description',
readonly: array_key_exists('group', $this->options),
enum: $this->getAvailableGroups(),
),
new SettingDefinition(
key: 'limit',
type: 'int',
default: (int)($this->options['limit'] ?? 0),
label: 'dashboard.widget_bookmarks:widget.bookmarks.setting.limit.label',
description: 'dashboard.widget_bookmarks:widget.bookmarks.setting.limit.description',
readonly: array_key_exists('limit', $this->options),
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$view = $this->backendViewFactory->create($context->request);
$view->assignMultiple([
'options' => $this->options,
'settings' => $context->settings,
'configuration' => $this->configuration,
]);
return new WidgetResult(
content: $view->render('Widget/BookmarksWidget'),
label: $this->resolveLabel($context->settings),
refreshable: true,
);
}
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create('@typo3/dashboard/widget/bookmarks-widget-element.js'),
];
}
private function getAvailableGroups(): array
{
$groups = [
'' => 'dashboard.widget_bookmarks:widget.bookmarks.setting.showAll',
];
// Only show groups that contain bookmarks
$groupsWithBookmarks = [];
foreach ($this->bookmarkService->getBookmarks() as $bookmark) {
$groupsWithBookmarks[$bookmark->groupId] = true;
}
foreach ($this->bookmarkService->getGroups() as $group) {
if (isset($groupsWithBookmarks[$group->id])) {
$groups[(string)$group->id] = $group->label;
}
}
return $groups;
}
private function resolveLabel(SettingsInterface $settings): ?string
{
$group = $settings->get('group');
if ($group !== '') {
$groupId = is_numeric($group) ? (int)$group : $group;
foreach ($this->bookmarkService->getGroups() as $bookmarkGroup) {
if ($bookmarkGroup->id === $groupId) {
$widgetTitle = $this->getLanguageService()->sL($this->configuration->getTitle());
return $widgetTitle . ': ' . $bookmarkGroup->label;
}
}
}
// Fall back to default widget title
return null;
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case a widget should have a button in the footer of the widget, this button must implement this interface.
*/
interface ButtonProviderInterface
{
/**
* This method should return the title that will be shown as the text on the button. As the title will be
* translated within the template, you can also return a localization string like
* 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:button'
*/
public function getTitle(): string;
/**
* Return the link
*/
public function getLink(): string;
/**
* Specify the target of the link like '_blank'
*/
public function getTarget(): string;
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* Defines API for provider, used for chart widgets.
*/
interface ChartDataProviderInterface
{
/**
* This method should provide the data for the graph.
* The data and options you have depend on the type of chart.
* More information can be found in the documentation of the specific type.
*
* @link https://www.chartjs.org/docs/latest/charts/bar.html#data-structure
* @link https://www.chartjs.org/docs/latest/charts/doughnut.html#data-structure
*/
public function getChartData(): array;
}
+73
View File
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
/**
* Concrete CTA button implementation
*
* Shows a widget with a CTA button to easily go to a specific page or do a specific action. You can add a button to the
* widget by defining a button provider.
*
* The following options are available during registration:
* - text string Adds a text to the widget to give some more background information about
* what a user can expect when clicking the button. You can either enter a
* normal string or a translation string (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text)
* @see ButtonProviderInterface
*/
class CtaWidget implements WidgetInterface, RequestAwareWidgetInterface
{
/**
* @var array{text: string}
*/
private readonly array $options;
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
array $options = [],
) {
$this->options = array_merge(['text' => ''], $options);
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'text' => $this->options['text'],
'options' => $this->options,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
]);
return $view->render('Widget/CtaWidget');
}
public function getOptions(): array
{
return $this->options;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
/**
* Concrete Doughnut Chart widget implementation
*
* Shows a widget with a doughnut chart. The data for this chart will be provided by the data provider you will set.
* You can add a button to the widget by defining a button provider.
*
* There are no options available for this widget
*
* @see ChartDataProviderInterface
* @see ButtonProviderInterface
*/
class DoughnutChartWidget implements WidgetInterface, RequestAwareWidgetInterface, EventDataInterface, AdditionalCssInterface, JavaScriptInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ChartDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'button' => $this->buttonProvider,
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/ChartWidget');
}
public function getEventData(): array
{
return [
'graphConfig' => [
'type' => 'doughnut',
'options' => [
'maintainAspectRatio' => false,
'plugins' => [
'legend' => [
'display' => true,
'position' => 'bottom',
],
],
'cutoutPercentage' => 60,
],
'data' => $this->dataProvider->getChartData(),
],
];
}
public function getCssFiles(): array
{
return [];
}
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create('@typo3/dashboard/contrib/chartjs.js'),
JavaScriptModuleInstruction::create('@typo3/dashboard/chart-initializer.js'),
];
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case HTML element shall contain additional attributes
*/
interface ElementAttributesInterface
{
/**
* @return array<string, string|null>
*/
public function getElementAttributes(): array;
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case a widget should provide additional data as JSON payload, the widget must implement this interface.
*/
interface EventDataInterface
{
/**
* This method returns data which should be sent to the widget as JSON encoded value.
*/
public function getEventData(): array;
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
/**
* Provides potential JavaScript declarations to be loaded/initialized for a particular widget.
*/
interface JavaScriptInterface
{
/**
* @return list<JavaScriptModuleInstruction>
*/
public function getJavaScriptModuleInstructions(): array;
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Dashboard\Widgets\Provider\LatestBeLoginsDataProvider;
/**
* This widget will show a list of recent backend user logins.
*
* The list contains:
* - backend user avatar and name
* - login time
*
* The following options are available during registration:
* - limit `int` number of logins to display
*/
readonly class LatestBeLoginsWidget implements WidgetRendererInterface, AdminOnlyWidgetInterface
{
public function __construct(
private BackendViewFactory $backendViewFactory,
private LatestBeLoginsDataProvider $dataProvider,
private ButtonProviderInterface $buttonProvider,
private WidgetConfigurationInterface $configuration,
) {}
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'limit',
type: 'int',
default: 10,
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.settings.limit',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.settings.limit.description',
options: [
'min' => 1,
],
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$limit = $context->settings->get('limit');
$items = $this->dataProvider->getItems($limit);
$view = $this->backendViewFactory->create($context->request);
$view->assignMultiple([
'items' => $items,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
]);
return new WidgetResult(
content: $view->render('Widget/LatestBeLoginsWidget'),
refreshable: true,
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* This widget will show a list of pages where latest changes in pages and tt_content
* where made. The sys_history is used to get the latest changes.
*
* The list contains:
* - datetime of change
* - user (avatar, icon, name and realName)
* - page title and rootline
* - controls (show history, view webpage, edit page content, edit page properties)
*
* The following options are available during registration:
* - limit int number of pages to show in list
* - historyLimit int number of sys_history records to be fetched in order
* to find limit number of pages. Increase this value
* if number of pages in list is not achieved.
*/
readonly class LatestChangedPagesWidget implements WidgetRendererInterface
{
/**
* @var array{limit: int, historyLimit: int}
*/
private array $options;
public function __construct(
private BackendViewFactory $backendViewFactory,
private ConnectionPool $connectionPool,
private WidgetConfigurationInterface $configuration,
private SiteFinder $siteFinder,
array $options = [],
) {
$this->options = array_merge([
'limit' => 10,
'historyLimit' => 1000,
], $options);
}
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'restrictToCurrentUser',
type: 'bool',
default: false,
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.settings.restrictToCurrentUser',
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$restrictToCurrentUser = (bool)$context->settings->get('restrictToCurrentUser');
$sysHistoryEntries = $this->getSysHistoryEntries($this->options['historyLimit'], $restrictToCurrentUser);
$latestPages = $this->getLatestPagesFromSysHistory($sysHistoryEntries, $this->options['limit']);
$latestPages = $this->enrichPageInformation($latestPages);
$view = $this->backendViewFactory->create($context->request);
$view->assignMultiple([
'latestPages' => $latestPages,
'configuration' => $this->configuration,
'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
]);
return new WidgetResult(
content: $view->render('Widget/LatestChangedPagesWidget'),
refreshable: true,
label: $restrictToCurrentUser ? $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.title.restrictToCurrentUser') : null,
);
}
private function getSysHistoryEntries(int $limit, bool $restrictToCurrentUser): array
{
$queryBuilder = $this->getQueryBuilderSysHistory();
$workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id');
$queryBuilder
->select('tablename', 'recuid', 'tstamp', 'userid')
->from('sys_history')
->where(
$queryBuilder->expr()->in(
'tablename',
[
$queryBuilder->createNamedParameter('pages'),
$queryBuilder->createNamedParameter('tt_content'),
]
),
$queryBuilder->expr()->eq('workspace', $workspaceId),
)
->addOrderBy('tstamp', 'desc')
->setMaxResults($limit);
if ($restrictToCurrentUser) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq(
'userid',
$queryBuilder->createNamedParameter($this->getBackendUser()->user['uid'])
),
);
}
return $queryBuilder
->executeQuery()
->fetchAllAssociative();
}
private function getLatestPagesFromSysHistory(array $history, int $limit): array
{
$latestPages = [];
foreach ($history as $historyEntry) {
$pageId = $historyEntry['recuid'];
if ($historyEntry['tablename'] === 'tt_content') {
$pageId = $this->getPageOfContentElement($historyEntry['recuid']);
}
if (!$pageId || isset($latestPages[$pageId])) {
continue;
}
$pageRecord = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
if ($pageRecord === false || $pageRecord === []) {
// Backend user has no access to show page information. Dismiss this page.
continue;
}
$latestPages[$pageId]['history'] = $historyEntry;
$pageRecord['_uid'] = $pageRecord['language_tag'] > 0 ? $pageRecord['l10n_parent'] : $pageRecord['uid'];
$latestPages[$pageId]['pageRecord'] = $pageRecord;
try {
$latestPages[$pageId]['siteLanguage'] = $this->siteFinder->getSiteByPageId($pageRecord['_uid'])->getLanguageById($pageRecord['language_tag']);
} catch (SiteNotFoundException $exception) {
$latestPages[$pageId]['siteLanguage'] = null;
}
// Override tstamp of pageRecord with tstamp from history record if newer
$latestPages[$pageId]['pageRecord']['tstamp'] = max($historyEntry['tstamp'], $latestPages[$pageId]['pageRecord']['tstamp']);
if (count($latestPages) >= $limit) {
break;
}
}
return $latestPages;
}
private function enrichPageInformation(array $latestPages): array
{
$userNames = BackendUtility::getUserNames('username,realName,uid');
foreach ($latestPages as $pageId => &$page) {
$page['rootline'] = $this->getRootLine($pageId);
$uriPageId = $page['pageRecord']['language_tag'] > 0 ? $page['pageRecord']['l10n_parent'] : $page['pageRecord']['uid'];
$page['viewLink'] = (string)PreviewUriBuilder::create($page['pageRecord'])
->withRootLine(BackendUtility::BEgetRootLine($uriPageId))
->buildUri();
$page['userName'] = $userNames[$page['history']['userid']]['username'] ?? '';
$page['realName'] = $userNames[$page['history']['userid']]['realName'] ?? '';
}
return $latestPages;
}
private function getPageOfContentElement(int $uid): ?int
{
$queryBuilder = $this->getQueryBuilderForContentElements();
$contentRecord = $queryBuilder
->select('pid', 'sys_language_uid')
->from('tt_content')
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)))
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$contentRecord) {
// Content element has been deleted
return null;
}
$queryBuilder = $this->getQueryBuilderForPages();
if ((int)$contentRecord['language_tag'] > 0) {
return $queryBuilder
->select('uid')
->from('pages')
->where(
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($contentRecord['pid'], Connection::PARAM_INT)),
$queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter($contentRecord['language_tag'], Connection::PARAM_INT))
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative()['uid'] ?? null;
}
return $queryBuilder
->select('uid')
->from('pages')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($contentRecord['pid'], Connection::PARAM_INT))
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative()['uid'] ?? null;
}
private function getRootLine(int $pageId): string
{
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
return implode(' / ', array_slice(
array_map(
static fn(array $page): string => $page['title'],
array_reverse($rootLine)
),
0,
-1
));
}
private function getQueryBuilderSysHistory(): QueryBuilder
{
$queryBuilder = $this->connectionPool->getConnectionForTable('sys_history')->createQueryBuilder();
return $queryBuilder;
}
private function getQueryBuilderForContentElements(): QueryBuilder
{
$queryBuilder = $this->connectionPool->getConnectionForTable('tt_content')->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
return $queryBuilder;
}
private function getQueryBuilderForPages(): QueryBuilder
{
$queryBuilder = $this->connectionPool->getConnectionForTable('pages')->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
return $queryBuilder;
}
public function getOptions(): array
{
return $this->options;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* The data provider of a ListWidget, should implement this interface
*/
interface ListDataProviderInterface
{
/**
* Return the items to be shown. This should be an array like ['item 1', 'item 2', 'item 3']. This is a
* real simple list of items.
*/
public function getItems(): array;
}
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
/**
* Concrete List Widget implementation
*
* The widget will show a simple list with items provided by a data provider. You can add a button to the widget by
* defining a button provider.
*
* There are no options available for this widget
*
* @see ListDataProviderInterface
* @see ButtonProviderInterface
*/
class ListWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ListDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'items' => $this->getItems(),
'options' => $this->options,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
]);
return $view->render('Widget/ListWidget');
}
protected function getItems(): array
{
return $this->dataProvider->getItems();
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* The dataprovider of a NumberWithIcon widget should implement this interface
*/
interface NumberWithIconDataProviderInterface
{
/**
* Return the number that should be shown in the widget
*/
public function getNumber(): int;
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
/**
* Concrete Number with Icon implementation
*
* The widget will show widget with an icon, a number, a title and a subtitle. The number is provided by a data
* provider.
*
* The following options are available during registration:
* - icon string The icon-identifier of the icon that should be shown in the widget. You should
* register your icon with the Icon API
* - title string The main title that will be shown in the widget as an explanation of the shown number.
* You can either enter a normal string or a translation string
* (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title)
* - subtitle string The subtitle that will give some additional information about the number and title.
* You can either enter a normal string or a translation string
* (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle)
*
* @see NumberWithIconDataProviderInterface
*/
class NumberWithIconWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly NumberWithIconDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'icon' => $this->options['icon'] ?? '',
'title' => $this->options['title'] ?? '',
'subtitle' => $this->options['subtitle'] ?? '',
'number' => $this->dataProvider->getNumber(),
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/NumberWithIconWidget');
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface;
/**
* Provides a button for the footer of a widget
*/
readonly class ButtonProvider implements ButtonProviderInterface
{
public function __construct(
private string $title,
private string $link,
private string $target = ''
) {}
public function getTitle(): string
{
return $this->title;
}
public function getLink(): string
{
return $this->link;
}
public function getTarget(): string
{
return $this->target;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
/**
* Data provider for "Latest backend logins" widget.
* Fetches successful backend user login entries from sys_log.
*/
readonly class LatestBeLoginsDataProvider
{
public function __construct(
private ConnectionPool $connectionPool
) {}
public function getItems(int $limit = 10): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_log');
$logEntries = $queryBuilder
->select('uid', 'tstamp', 'userid', 'details')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'action',
$queryBuilder->createNamedParameter(SystemLogLoginAction::LOGIN, Connection::PARAM_INT)
)
)
->orderBy('tstamp', 'DESC')
->setMaxResults($limit)
->executeQuery()
->fetchAllAssociative();
// Enrich with user information
$items = [];
$userNames = BackendUtility::getUserNames('username,realName,uid');
foreach ($logEntries as $entry) {
$userId = (int)$entry['userid'];
$userInfo = $userNames[$userId] ?? null;
if ($userInfo !== null) {
$items[] = [
'timestamp' => (int)$entry['tstamp'],
'userId' => $userId,
'username' => $userInfo['username'] ?? '',
'realName' => $userInfo['realName'] ?? '',
'details' => $entry['details'],
];
}
}
return $items;
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Widgets\NumberWithIconDataProviderInterface;
class NumberOfFailedLoginsDataProvider implements NumberWithIconDataProviderInterface
{
public function getNumber(int $secondsBack = 86400): int
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
return (int)$queryBuilder->count('uid')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'action',
$queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, Connection::PARAM_INT)
),
$queryBuilder->expr()->neq(
'error',
$queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, Connection::PARAM_INT)
),
$queryBuilder->expr()->gt(
'tstamp',
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'] - $secondsBack, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface;
use TYPO3\CMS\Dashboard\Widgets\ElementAttributesInterface;
/**
* Provide link for sys log button.
* Check whether belog is enabled and add link to module.
* No link is returned if not enabled.
*/
readonly class SysLogButtonProvider implements ButtonProviderInterface, ElementAttributesInterface
{
public function __construct(
private string $title,
private string $target = '',
private string $channel = '',
) {}
public function getTitle(): string
{
return $this->title;
}
public function getLink(): string
{
return '';
}
public function getTarget(): string
{
return $this->target;
}
public function getElementAttributes(): array
{
if (!ExtensionManagementUtility::isLoaded('belog')) {
return [];
}
return [
'data-dispatch-action' => 'TYPO3.ModuleMenu.showModule',
'data-dispatch-args-list' => 'system_log,&'
. http_build_query(['constraint' => ['channel' => $this->channel ?: 'php']]),
];
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\WidgetApi;
use TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface;
/**
* Provides chart data for sys log errors.
*/
class SysLogErrorsDataProvider implements ChartDataProviderInterface
{
protected array $labels = [];
protected array $data = [];
/**
* @param int $days Number of days to gather information for.
*/
public function __construct(protected readonly int $days = 31) {}
public function getChartData(): array
{
$this->calculateDataForLastDays();
return [
'labels' => $this->labels,
'datasets' => [
[
'label' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.chart.dataSet.0'),
'backgroundColor' => WidgetApi::getDefaultChartColors()[0],
'border' => 0,
'data' => $this->data,
],
],
];
}
protected function getNumberOfErrorsInPeriod(int $start, int $end): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
return (int)$queryBuilder
->count('*')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::ERROR, Connection::PARAM_INT)
),
$queryBuilder->expr()->gte(
'tstamp',
$queryBuilder->createNamedParameter($start, Connection::PARAM_INT)
),
$queryBuilder->expr()->lte(
'tstamp',
$queryBuilder->createNamedParameter($end, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
protected function calculateDataForLastDays(): void
{
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d';
for ($daysBefore = $this->days; $daysBefore >= 0; $daysBefore--) {
$this->labels[] = date($format, (int)strtotime('-' . $daysBefore . ' day'));
$startPeriod = (int)strtotime('-' . $daysBefore . ' day 0:00:00');
$endPeriod = (int)strtotime('-' . $daysBefore . ' day 23:59:59');
$this->data[] = $this->getNumberOfErrorsInPeriod($startPeriod, $endPeriod);
}
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\WidgetApi;
use TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface;
readonly class TypeOfUsersChartDataProvider implements ChartDataProviderInterface
{
public function __construct(private LanguageServiceFactory $languageServiceFactory) {}
public function getChartData(): array
{
$languageService = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
$adminUsers = $this->getNumberOfUsers(true);
$normalUsers = $this->getNumberOfUsers(false);
return [
'labels' => [
$languageService->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.normalUsers'),
$languageService->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.adminUsers'),
],
'datasets' => [
[
'backgroundColor' => WidgetApi::getDefaultChartColors(),
'data' => [$normalUsers, $adminUsers],
],
],
];
}
protected function getNumberOfUsers(bool $admin = false): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
return (int)$queryBuilder
->count('*')
->from('be_users')
->where(
$queryBuilder->expr()->eq(
'admin',
$queryBuilder->createNamedParameter($admin ? 1 : 0, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
/**
* Interface for widgets that need the ServerRequestInterface Request.
* The setter is called immediately after class instantiation.
* Useful for Widgets that depend on a request, for instance when dealing
* with views based on BackendViewFactory.
*/
interface RequestAwareWidgetInterface
{
public function setRequest(ServerRequestInterface $request): void;
}
+195
View File
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Exception\InvalidRssFeedException;
/**
* Concrete RSS widget implementation
*
* The widget will show a certain number of items of the given RSS feed. The feed will be set by the feedUrl option. You
* can add a button to the widget by defining a button provider.
*
* The following options are available during registration:
* - feedUrl string Defines the URL or file providing the RSS Feed.
* This is read by the widget in order to fetch entries to show.
* - limit int default: 5 Defines how many RSS items should be shown.
* - lifetime int default: 43200 Defines how long to wait, in seconds, until fetching RSS Feed again
*
* @see ButtonProviderInterface
*/
readonly class RssWidget implements WidgetRendererInterface
{
public function __construct(
private WidgetConfigurationInterface $configuration,
#[Autowire(service: 'cache.dashboard.rss')]
private FrontendInterface $cache,
private BackendViewFactory $backendViewFactory,
private ?ButtonProviderInterface $buttonProvider = null,
/** @var array{limit?: int, lifeTime?: int, feedUrl?: string} */
private array $options = [],
) {}
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'label',
type: 'string',
default: '',
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.label.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.label.description',
readonly: array_key_exists('feedUrl', $this->options),
),
new SettingDefinition(
key: 'feedUrl',
type: 'url',
default: (string)($this->options['feedUrl'] ?? ''),
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.description',
readonly: array_key_exists('feedUrl', $this->options),
options: [
'pattern' => 'https?://.+',
],
),
new SettingDefinition(
key: 'limit',
type: 'int',
default: (int)($this->options['limit'] ?? 5),
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.description',
readonly: array_key_exists('limit', $this->options),
),
new SettingDefinition(
key: 'lifeTime',
type: 'int',
default: (int)($this->options['lifeTime'] ?? 43200),
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.description',
readonly: true,
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$view = $this->backendViewFactory->create($context->request);
$feedUrl = $context->settings->get('feedUrl');
$items = [];
if ($feedUrl) {
try {
$items = $this->getFeedItems($context->settings);
} catch (InvalidRssFeedException) {
$view->assign('invalidFeed', true);
}
}
$view->assignMultiple([
'feedUrl' => $feedUrl,
'items' => $items,
'settings' => $context->settings,
'options' => $this->options,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
]);
return new WidgetResult(
label: $context->settings->get('label') !== '' ? $context->settings->get('label') : null,
content: $view->render('Widget/RssWidget'),
refreshable: true,
);
}
protected function getFeedItems(SettingsInterface $settings): array
{
$cacheHash = md5($settings->get('feedUrl') . '-' . $settings->get('limit'));
if ($items = $this->cache->get($cacheHash)) {
return $items;
}
$feedContent = GeneralUtility::getUrl($settings->get('feedUrl'));
if ($feedContent === false) {
throw new InvalidRssFeedException('RSS URL could not be fetched', 1573385431);
}
try {
$feedXml = simplexml_load_string($feedContent);
} catch (\Exception $e) {
throw new InvalidRssFeedException('Received RSS feed could not be parsed.', 1573385432, $e);
}
$items = match ($this->determineFeedType($feedXml)) {
'atom' => $this->parseAtomFeed($feedXml),
'rss' => $this->parseRssFeed($feedXml),
default => []
};
usort($items, static function ($item1, $item2) {
return new \DateTime($item2['pubDate']) <=> new \DateTime($item1['pubDate']);
});
$items = array_slice($items, 0, (int)$settings->get('limit'));
$this->cache->set($cacheHash, $items, ['dashboard_rss'], (int)$settings->get('lifeTime'));
return $items;
}
protected function determineFeedType(\SimpleXMLElement $feedXml): string
{
return $feedXml->getName() === 'feed' ? 'atom' : 'rss';
}
protected function parseRssFeed(\SimpleXMLElement $rssFeed): array
{
$items = [];
foreach ($rssFeed->channel->item as $item) {
$items[] = [
'title' => trim((string)$item->title),
'link' => trim((string)$item->link),
'pubDate' => trim((string)$item->pubDate),
'description' => trim(strip_tags((string)$item->description)),
];
}
return $items;
}
protected function parseAtomFeed(\SimpleXMLElement $atomFeed): array
{
$items = [];
foreach ($atomFeed->entry as $entry) {
$items[] = [
'title' => trim((string)$entry->title),
'link' => trim((string)($entry->link['href'] ?? '')),
'pubDate' => trim((string)($entry->published ?? $entry->updated ?? '')),
'description' => trim(strip_tags((string)($entry->summary ?? $entry->content ?? ''))),
'author' => [
'name' => trim((string)($entry->author->name ?? '')),
'email' => trim((string)($entry->author->email ?? '')),
'url' => trim((string)($entry->author->url ?? '')),
],
];
}
return $items;
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Information\Typo3Version;
/**
* Concrete TYPO3 information widget
*
* This widget will give some general information about TYPO3 version and the version installed.
*
* There are no options available for this widget
*/
class T3GeneralInformationWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly BackendViewFactory $backendViewFactory,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$typo3Information = new Typo3Information();
$typo3Version = new Typo3Version();
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'title' => 'TYPO3 CMS ' . $typo3Version->getVersion(),
'copyrightYear' => $typo3Information->getCopyrightYear(),
'currentVersion' => $typo3Version->getVersion(),
'donationUrl' => $typo3Information::URL_DONATE,
'copyRightNotice' => $typo3Information->getCopyrightNotice(),
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/T3GeneralInformationWidget');
}
public function getOptions(): array
{
return $this->options;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
readonly class WidgetConfiguration implements WidgetConfigurationInterface
{
/**
* @throws \InvalidArgumentException If non valid parameters were provided.
*/
public function __construct(
private string $identifier,
private string $serviceName,
private array $groupNames,
private string $title,
private string $description,
private string $iconIdentifier,
private string $height,
private string $width,
private bool $adminOnly = false,
) {
$allowedSizes = ['small', 'medium', 'large'];
if (!in_array($height, $allowedSizes, true)) {
throw new \InvalidArgumentException('Height of widgets has to be small, medium or large', 1584778196);
}
if (!in_array($width, $allowedSizes, true)) {
throw new \InvalidArgumentException('Width of widgets has to be small, medium or large', 1585249769);
}
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getServiceName(): string
{
return $this->serviceName;
}
public function getGroupNames(): array
{
return $this->groupNames;
}
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
public function getIconIdentifier(): string
{
return $this->iconIdentifier;
}
public function getHeight(): string
{
return $this->height;
}
public function getWidth(): string
{
return $this->width;
}
public function isAdminOnly(): bool
{
return $this->adminOnly;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* Defines API of configuration for a widget.
* This is separated by concrete implementation of a widget (WidgetInterface).
* The configuration is used to generate UX, and other stuff.
*/
interface WidgetConfigurationInterface
{
/**
* Returns the unique identifier of a widget
*/
public function getIdentifier(): string;
/**
* Returns the service name providing the widget implementation
*/
public function getServiceName(): string;
/**
* Returns array of group names associated to this widget
*/
public function getGroupNames(): array;
/**
* Returns the title of a widget, this is used for the widget selector
*/
public function getTitle(): string;
/**
* Returns the description of a widget, this is used for the widget selector
*/
public function getDescription(): string;
/**
* Returns the icon identifier of a widget, this is used for the widget selector
*/
public function getIconIdentifier(): string;
/**
* Returns the height of a widget (small, medium, large)
*/
public function getHeight(): string;
/**
* Returns the width of a widget (small, medium, large)
*/
public function getWidth(): string;
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Settings\SettingsInterface;
/**
* Widget context containing all necessary data for widget rendering.
*
* This readonly value object encapsulates all the context information needed
* by widgets during rendering, including:
* - Widget instance identifier and raw configuration data
* - Widget configuration and settings
* - Current HTTP request context
*
* The widget context is passed to widgets implementing WidgetRendererInterface
* and provides a clean interface for accessing widget-specific data and settings.
*/
final readonly class WidgetContext
{
public function __construct(
public string $identifier,
public array $rawData,
public WidgetConfigurationInterface $configuration,
public SettingsInterface $settings,
public ServerRequestInterface $request,
) {}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* The WidgetInterface is the base interface for all kind of widgets.
* All widgets must implement this interface.
* It contains the methods which are required for all widgets.
*/
interface WidgetInterface
{
/**
* This method returns the content of a widget. The returned markup will be delivered
* by an AJAX call and will not be escaped.
* Be aware of XSS and ensure that the content is well encoded.
*/
public function renderWidgetContent(): string;
/**
* This method returns the options of the widget as set in the registration.
*/
public function getOptions(): array;
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
use TYPO3\CMS\Core\Settings\SettingDefinition;
/**
* The WidgetRendererInterface is the (new) base interface for all kind of widgets.
* All widgets should implement this interface. (but can also implement WidgetInterface for the time being)
* It contains the methods which are required for all widgets.
*/
interface WidgetRendererInterface
{
/**
* This method returns the content of a widget. The returned markup will be delivered
* by an AJAX call and will not be escaped.
* Be aware of XSS and ensure that the content is well encoded.
*/
public function renderWidget(WidgetContext $context): WidgetResult;
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array;
}
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* Widget rendering result containing content and metadata.
*
* This readonly value object represents the result of widget rendering,
* containing the rendered HTML content and additional metadata about
* the widget's capabilities and state.
*
* The result includes:
* - HTML content to be displayed in the dashboard
* - Optional custom label overriding the widget's default title
* - Refreshable flag indicating whether the widget supports refresh operations
*/
final readonly class WidgetResult
{
public function __construct(
public string $content,
public ?string $label = null,
public bool $refreshable = false,
) {}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
use TYPO3\CMS\Dashboard\Controller\DashboardAjaxController;
return [
// Dashboards
'dashboard_dashboards_get' => [
'path' => '/dashboard/dashboards/get',
'target' => DashboardAjaxController::class . '::getDashboards',
'methods' => ['GET'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_dashboard_add' => [
'path' => '/dashboard/dashboard/add',
'target' => DashboardAjaxController::class . '::addDashboard',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_dashboard_edit' => [
'path' => '/dashboard/dashboard/edit',
'target' => DashboardAjaxController::class . '::editDashboard',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_dashboard_update' => [
'path' => '/dashboard/dashboard/update',
'target' => DashboardAjaxController::class . '::updateDashboard',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_dashboard_delete' => [
'path' => '/dashboard/dashboard/delete',
'target' => DashboardAjaxController::class . '::deleteDashboard',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
// Presets
'dashboard_presets_get' => [
'path' => '/dashboard/presets/get',
'target' => DashboardAjaxController::class . '::getPresets',
'methods' => ['GET'],
'inheritAccessFromModule' => 'dashboard',
],
// Categories
'dashboard_categories_get' => [
'path' => '/dashboard/categories/get',
'target' => DashboardAjaxController::class . '::getCategories',
'methods' => ['GET'],
'inheritAccessFromModule' => 'dashboard',
],
// Widgets
'dashboard_widget_get' => [
'path' => '/dashboard/widget/get',
'target' => DashboardAjaxController::class . '::getWidget',
'methods' => ['GET'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_widget_add' => [
'path' => '/dashboard/widget/add',
'target' => DashboardAjaxController::class . '::addWidget',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_widget_remove' => [
'path' => '/dashboard/widget/remove',
'target' => DashboardAjaxController::class . '::removeWidget',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_widget_settings_get' => [
'path' => '/dashboard/widget/settings/get',
'target' => DashboardAjaxController::class . '::getWidgetSettings',
'methods' => ['GET'],
'inheritAccessFromModule' => 'dashboard',
],
'dashboard_widget_settings_update' => [
'path' => '/dashboard/widget/settings/update',
'target' => DashboardAjaxController::class . '::updateWidgetSettings',
'methods' => ['POST'],
'inheritAccessFromModule' => 'dashboard',
],
];
@@ -0,0 +1,18 @@
<?php
return [
'default' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default',
'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default.description',
'iconIdentifier' => 'content-dashboard',
'defaultWidgets' => ['t3information', 'docGettingStarted'],
'showInWizard' => false,
],
'empty' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.empty',
'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.empty.description',
'iconIdentifier' => 'content-dashboard-empty',
'defaultWidgets' => [],
'showInWizard' => true,
],
];
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
return [
'general' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.general',
],
'systemInfo' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.system',
],
'typo3' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.typo3',
],
'news' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.news',
],
'documentation' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.documentation',
],
'content' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.content',
],
];
+22
View File
@@ -0,0 +1,22 @@
<?php
use TYPO3\CMS\Dashboard\Controller\DashboardController;
/**
* Definitions for modules provided by EXT:dashboard
*/
return [
'dashboard' => [
'position' => ['before' => '*'],
'standalone' => true,
'access' => 'user',
'path' => '/module/dashboard',
'iconIdentifier' => 'module-dashboard',
'labels' => 'dashboard.module',
'routes' => [
'_default' => [
'target' => DashboardController::class . '::mainAction',
],
],
],
];
+19
View File
@@ -0,0 +1,19 @@
<?php
return [
'dependencies' => [
'backend',
'core',
],
'imports' => [
'@typo3/dashboard/' => [
'path' => 'EXT:dashboard/Resources/Public/JavaScript/',
'exclude' => [
'EXT:dashboard/Resources/Public/JavaScript/Contrib/',
],
],
'chart.js' => 'EXT:dashboard/Resources/Public/JavaScript/Contrib/chartjs.js',
// legacy, has been renamed "chart.js"
'@typo3/dashboard/contrib/chartjs.js' => 'EXT:dashboard/Resources/Public/JavaScript/Contrib/chartjs.js',
],
];
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Dashboard;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
$containerBuilder->addCompilerPass(new DependencyInjection\DashboardWidgetPass('dashboard.widget'));
};
+252
View File
@@ -0,0 +1,252 @@
services:
_defaults:
autowire: true
autoconfigure: true
public: false
TYPO3\CMS\Dashboard\:
resource: '../Classes/*'
TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration:
autowire: false
dashboard.buttons.syslogErrors:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.buttonText'
dashboard.buttons.syslogUsers:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogUsers.buttonText'
$channel: 'user'
cache.dashboard.rss:
class: 'TYPO3\CMS\Core\Cache\Frontend\FrontendInterface'
factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
arguments:
$identifier: 'dashboard_rss'
dashboard.buttons.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems'
$link: 'https://news.typo3.com'
$target: '_blank'
dashboard.buttons.t3securityAdvisories:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.moreItems'
$link: 'https://typo3.org/help/security-advisories'
$target: '_blank'
dashboard.buttons.docGettingStarted:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.content.label'
$link: 'https://docs.typo3.org/m/typo3/tutorial-getting-started/main/en-us/Index.html'
$target: '_blank'
dashboard.buttons.docTypoScriptReference:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.content.label'
$link: 'https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Index.html'
$target: '_blank'
dashboard.buttons.docTSconfig:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.content.label'
$link: 'https://docs.typo3.org/m/typo3/reference-tsconfig/main/en-us/Index.html'
$target: '_blank'
dashboard.widget.rss:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
tags:
- name: dashboard.widget
identifier: 'rss'
groupNames: 'news'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.description'
iconIdentifier: 'content-widget-rss'
height: 'large'
width: 'medium'
dashboard.widget.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
arguments:
$buttonProvider: '@dashboard.buttons.t3news'
$options:
feedUrl: 'https://typo3.org/rss'
tags:
- name: dashboard.widget
identifier: 't3news'
groupNames: 'news'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description'
iconIdentifier: 'content-widget-rss'
height: 'large'
width: 'medium'
dashboard.widget.sysLogErrors:
class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget'
arguments:
$dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider'
$buttonProvider: '@dashboard.buttons.syslogErrors'
$options:
refreshAvailable: true
tags:
- name: dashboard.widget
identifier: 'sysLogErrors'
groupNames: 'systemInfo'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.description'
iconIdentifier: 'content-widget-chart-bar'
height: 'medium'
width: 'medium'
dashboard.widget.docGettingStarted:
class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget'
arguments:
$buttonProvider: '@dashboard.buttons.docGettingStarted'
$options:
text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text'
tags:
- name: dashboard.widget
identifier: 'docGettingStarted'
groupNames: 'documentation'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.description'
iconIdentifier: 'content-widget-text'
height: 'small'
dashboard.widget.docTypoScriptReference:
class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget'
arguments:
$buttonProvider: '@dashboard.buttons.docTypoScriptReference'
$options:
text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.text'
tags:
- name: dashboard.widget
identifier: 'docTypoScriptReference'
groupNames: 'documentation'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.description'
iconIdentifier: 'content-widget-text'
height: 'small'
dashboard.widget.docTSconfig:
class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget'
arguments:
$buttonProvider: '@dashboard.buttons.docTSconfig'
$options:
text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.text'
tags:
- name: dashboard.widget
identifier: 'docTSconfig'
groupNames: 'documentation'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.description'
iconIdentifier: 'content-widget-text'
height: 'small'
dashboard.widget.t3information:
class: 'TYPO3\CMS\Dashboard\Widgets\T3GeneralInformationWidget'
tags:
- name: dashboard.widget
identifier: 't3information'
groupNames: 'general'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.description'
iconIdentifier: 'content-widget-text'
height: 'medium'
width: 'medium'
dashboard.widget.typeOfUsers:
class: 'TYPO3\CMS\Dashboard\Widgets\DoughnutChartWidget'
arguments:
$dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\TypeOfUsersChartDataProvider'
tags:
- name: dashboard.widget
identifier: 'typeOfUsers'
groupNames: 'systemInfo'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.description'
iconIdentifier: 'content-widget-chart-pie'
height: 'medium'
dashboard.widget.t3securityAdvisories:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
arguments:
$buttonProvider: '@dashboard.buttons.t3securityAdvisories'
$options:
feedUrl: 'https://typo3.org/rss-security'
tags:
- name: dashboard.widget
identifier: 't3securityAdvisories'
groupNames: 'news'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.description'
iconIdentifier: 'content-widget-rss'
height: 'large'
width: 'medium'
dashboard.widget.failedLogins:
class: 'TYPO3\CMS\Dashboard\Widgets\NumberWithIconWidget'
arguments:
$dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\NumberOfFailedLoginsDataProvider'
$options:
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title'
subtitle: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle'
icon: 'content-elements-login'
tags:
- name: dashboard.widget
identifier: 'failedLogins'
groupNames: 'systemInfo'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.description'
iconIdentifier: 'content-widget-number'
dashboard.widget.bookmarks:
class: 'TYPO3\CMS\Dashboard\Widgets\BookmarksWidget'
tags:
- name: dashboard.widget
identifier: 'bookmarks'
groupNames: 'content'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_bookmarks.xlf:widget.bookmarks.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_bookmarks.xlf:widget.bookmarks.description'
iconIdentifier: 'content-bookmark'
height: 'medium'
width: 'medium'
dashboard.widget.latestChangedPages:
class: 'TYPO3\CMS\Dashboard\Widgets\LatestChangedPagesWidget'
arguments:
$options:
refreshAvailable: true
tags:
- name: dashboard.widget
identifier: 'latestChangedPages'
groupNames: 'content'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.description'
iconIdentifier: 'content-widget-list'
height: 'medium'
width: 'medium'
dashboard.widget.latestBeLogins:
class: 'TYPO3\CMS\Dashboard\Widgets\LatestBeLoginsWidget'
arguments:
$dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\LatestBeLoginsDataProvider'
$buttonProvider: '@dashboard.buttons.syslogUsers'
tags:
- name: dashboard.widget
identifier: 'latestBeLogins'
groupNames: 'systemInfo'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.description'
iconIdentifier: 'content-widget-list'
height: 'medium'
width: 'small'
+26
View File
@@ -0,0 +1,26 @@
<?php
defined('TYPO3') or die();
call_user_func(static function () {
$additionalColumns = [
'availableWidgets' => [
'label' => 'core.tca:availableWidgets',
'config' => [
'type' => 'select',
'renderType' => 'selectCheckBox',
'itemsProcFunc' => \TYPO3\CMS\Dashboard\WidgetRegistry::class . '->widgetItemsProcFunc',
'size' => 5,
'autoSizeMax' => 50,
],
],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('be_groups', $additionalColumns);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
'be_groups',
'availableWidgets',
'',
'after:groupMods'
);
});
+63
View File
@@ -0,0 +1,63 @@
<?php
return [
'ctrl' => [
'title' => 'dashboard.db:be_dashboard',
'label' => 'title',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'adminOnly' => true,
'rootLevel' => 1,
'delete' => 'deleted',
'hideTable' => true,
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
],
'default_sortby' => 'crdate DESC',
'typeicon_classes' => [
'default' => 'content-dashboard',
],
],
'columns' => [
// The owner of the dashboard
'cruser_id' => [
'config' => [
'type' => 'passthrough',
],
],
'identifier' => [
'label' => 'dashboard.db:identifier',
'config' => [
'type' => 'input',
'size' => 30,
'max' => 255,
'required' => true,
],
],
'title' => [
'label' => 'dashboard.db:title',
'config' => [
'type' => 'input',
'size' => 30,
'max' => 255,
'required' => true,
],
],
],
'types' => [
'1' => [
'showitem' => '
--div--;core.form.tabs:general,
identifier,title,
--div--;core.form.tabs:access,
hidden, --palette--;;timeRestriction,
--div--;core.form.tabs:extended,
',
],
],
'palettes' => [
'timeRestriction' => ['showitem' => 'starttime, endtime'],
],
];
+20
View File
@@ -0,0 +1,20 @@
.. include:: /Includes.rst.txt
.. _configuration:
=============
Configuration
=============
Target group: **Developers** and **Integrators**
.. toctree::
:maxdepth: 3
:titlesonly:
WidgetRegistration
WidgetGroupCreation
WidgetPresets
WidgetSettings
WidgetTemplate
PermissionHandlingOfWidgets
@@ -0,0 +1,17 @@
.. include:: /Includes.rst.txt
.. _permission-handling-of-widgets:
======================
Permissions of widgets
======================
Backend users marked as administrator have always access to all registered widgets.
Other backend users can be restricted via :guilabel:`Access List > Dashboard widgets` inside of user groups.
Each widget needs to be explicitly allowed.
.. figure:: /Images/AccessRestriction.png
:align: center
Granting access to dashboard widgets for backend users.
@@ -0,0 +1,45 @@
.. include:: /Includes.rst.txt
.. _create-widget-group:
===================
Create widget group
===================
Widget groups are used to group widgets into tabs.
This will have an effect when adding new widgets to an dashboard.
See :ref:`adding-widgets` to get an idea of the UI.
Groups are defined as PHP array:
.. code-block:: php
:caption: Example from EXT:dashboard/Configuration/Backend/DashboardWidgetGroups.php
<?php
return [
'general' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.general',
],
'systemInfo' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.system',
],
'typo3' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.typo3',
],
'news' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.news',
],
'documentation' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.documentation',
],
];
The file has to return an array of groups.
Each group consists of an array key used as identifier and an single option :php:`title`.
The title will be processed through translation and can be an ``LLL`` reference.
Each extension can create arbitrary widget groups.
Widgets can be assigned to multiple groups using the :confval:`widget-tag-groupNames`.
Please read :ref:`register-new-widget` to understand how this is done.
@@ -0,0 +1,124 @@
.. include:: /Includes.rst.txt
.. _dashboard-presets:
=================
Dashboard Presets
=================
It is possible to configure presets of dashboards.
The extension already ships a ``default`` as well as an ``empty`` dashboard preset.
.. _create-preset:
Create preset
-------------
New presets can be configured:
.. code-block:: php
:caption: Example from EXT:dashboard/Configuration/Backend/DashboardPresets.php
<?php
return [
'default' => [
'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default',
'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default.description',
'iconIdentifier' => 'content-dashboard',
'defaultWidgets' => [
't3information',
't3news',
'docGettingStarted',
[
'identifier' => 'rss',
'settings' => [
'label' => 'My RSS Feed',
'feedUrl' => 'https://typo3.org/rss',
'limit' => 10,
],
],
],
'showInWizard' => false,
],
];
The file has to return an array with all presets.
Each prefix itself is an array, with an identifier as key.
The identifier is used to configure presets for users, see :ref:`configure-preset-for-user`.
Each preset consists of the following options:
.. php:class:: TYPO3\CMS\Dashboard\DashboardPreset
.. confval:: title
:type: string
:name: widget-presets-title
The title used for the preset. E.g. a ``LLL:EXT:`` reference..
.. confval:: description
:type: string
:name: widget-presets-description
The description used for the preset. E.g. a ``LLL:EXT:`` reference..
.. confval:: iconIdentifier
:type: string
:name: widget-presets-iconIdentifier
The identifier of the icon to use.
.. confval:: defaultWidgets
:type: array
:name: widget-presets-defaultWidgets
An array of widget identifiers, or fine-grained structure, that should be part of the dashboard preset.
Widgets are always filtered by permissions of each user.
Only widgets with access are actually part of the dashboard.
Have a look at :ref:`permission-handling-of-widgets` to understand how to handle permissions.
.. confval:: showInWizard
:type: bool
:name: widget-presets-showInWizard
Boolean value to indicate, whether this preset should be visible in the wizard,
when creating new dashboards, see :ref:`adding-dashboard`.
This can be disabled, to add presets via :ref:`configure-preset-for-user`, without
showing up in the wizard.
.. _configure-preset-for-user:
Configure preset for user
-------------------------
To define the default preset for a backend user, the following User TSconfig can be added:
.. code-block:: typoscript
options.dashboard.dashboardPresetsForNewUsers = default
Where ``default`` is the identifier of the preset.
Even a comma separated list of identifiers is possible:
.. code-block:: typoscript
options.dashboard.dashboardPresetsForNewUsers = default, companyDefault
It is also possible to add another dashboard to the set of dashboards:
.. code-block:: typoscript
options.dashboard.dashboardPresetsForNewUsers := addToList(anotherOne)
If nothing is configured, ``default`` will be used as identifier.
.. seealso::
:ref:`t3tsref:userthetsconfigfield` section of TSconfig manual
explains how to set or register TSconfig for user.
:ref:`t3tsref:typoscript-syntax-syntax-value-modification` explains the usage of
:typoscript:`:=` TypoScript operator.
@@ -0,0 +1,326 @@
.. include:: /Includes.rst.txt
Widgets need to be provided by an extension, e.g. by ext:dashboard.
They are provided as a PHP class with specific feature sets.
Each of the widgets can be registered with different configurations as documented below.
.. include:: /Shared/DifferenceRegistrationAndImplementation.rst.txt
The below example will use the RSS Widget as a concrete example.
.. _register-new-widget:
===================
Register new Widget
===================
Registration happens through :ref:`Dependency Injection <t3coreapi:DependencyInjection>`
either in :file:`Services.yaml` or :file:`Services.php`.
Both files can exist and will be merged.
:file:`Services.yaml` is recommended and easier to write,
while :file:`Services.php` provide way more flexibility.
.. _register-new-widget-naming:
Naming widgets
--------------
Widgets receive a name in form of ``dashboard.widget.vendor.ext_key.widgetName``.
``vendor``
Should be a snaked version of composer vendor.
``ext_key``
Should be the extension key.
This prevents naming conflicts if multiple 3rd Party extensions are installed.
.. _register-new-widget-services:
Services.yaml file
------------------
In order to turn the PHP class :php:`\TYPO3\CMS\Dashboard\Widgets\RssWidget` into an actual widget,
the following service registration can be used:
.. code-block:: yaml
:caption: Excerpt from EXT:dashboard/Configuration/Services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
TYPO3\CMS\Dashboard\:
resource: '../Classes/*'
dashboard.widget.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
arguments:
$buttonProvider: '@dashboard.buttons.t3news'
$options:
feedUrl: 'https://www.typo3.org/rss'
tags:
- name: dashboard.widget
identifier: 't3news'
groupNames: 'news'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description'
iconIdentifier: 'content-widget-rss'
height: 'large'
width: 'medium'
The beginning of the file is not related to the widget itself, but dependency injection in general,
see: :ref:`t3coreapi:configure-dependency-injection-in-extensions`.
.. _register-new-widget-service-configuration:
Service configuration
"""""""""""""""""""""
The last block configured a service called :yaml:`dashboard.widget.t3news`.
This service is configured to use the existing PHP class :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget`.
When creating the instance of this class, an array is provided for the constructor argument :php:`$options`.
This way the same PHP class can be used with different configuration to create new widgets.
The following keys are defined for the service:
.. confval:: class
:type: string
:name: widget-class
:Example: :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget`
Defines the concrete PHP class to use as the implementation of the widget.
.. confval:: arguments
:type: map
:name: widget-arguments
A set of key-value pairs, where the keys are the argument names and the
values are the corresponding argument values. The specific arguments depend
on the widget being configured, and each widget can define custom arguments.
Documentation for the provided widgets is available at :ref:`widgets`.
.. confval:: tags
:type: array of dictionaries
:name: widget-tags
Registers the service as an actual widget for :composer:`typo3/cms-dashboard`. Each entry in
the array is a dictionary that can include various properties like name,
identifier, groupNames, and so on, used to categorize and identify the widget.
See :ref:`register-new-widget-tags-section`.
.. _register-new-widget-tags-section:
Tags Section
""""""""""""
In order to turn the instance into a widget, the tag `dashboard.widget` is configured in `tags` section.
The following options are mandatory and need to be provided:
.. confval:: name
:type: string
:name: widget-tag-name
:required:
:Example: `dashboard.widget`
Always has to be `dashboard.widget`.
Defines that this tag configures the service to be registered as a widget for
ext:dashboard.
.. confval:: identifier
:type: string
:name: widget-tag-identifier
:required:
:Example: `t3news`
Used to store which widgets are currently assigned to dashboards.
Furthermore, it is used to allow access control, see :ref:`permission-handling-of-widgets`.
.. confval:: groupNames
:type: string (comma-separated)
:name: widget-tag-groupNames
:required:
:Example: `news`
Defines which groups should contain the widget.
Used when adding widgets to a dashboard to group related widgets in tabs.
Multiple names can be defined as a comma-separated string, e.g.: `typo3, general`.
See :ref:`create-widget-group` regarding how to create new widget groups.
There is no difference between custom groups and existing groups.
Widgets are registered to all groups by their name.
.. confval:: title
:type: string (language reference)
:name: widget-tag-title
:required:
:Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title`
Defines the title of the widget. Language references are resolved.
.. confval:: description
:type: string (language reference)
:name: widget-tag-description
:required:
:Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description`
Defines the description of the widget. Language references are resolved.
.. confval:: iconIdentifier
:type: string
:name: widget-tag-iconIdentifier
:required:
:Example: `content-widget-rss`
One of the registered icons.
Icons can be registered through :ref:`t3coreapi:icon`.
The following options are optional and have default values which will be used if not defined:
.. confval:: height
:type: string
:name: widget-tag-height
:Example: `large`
Has to be a string value: `large`, `medium`, or `small`.
.. confval:: width
:type: string
:name: widget-tag-width
:Example: `medium`
Has to be a string value: `large`, `medium`, or `small`.
.. _register-new-widget-splitting:
Splitting up Services.yaml
--------------------------
In case the :file:`Services.yaml` is getting to large, it can be split up.
The official documentation can be found at `symfony.com <https://symfony.com/doc/current/service_container/import.html>`__.
An example to split up all Widget related configuration would look like:
.. code-block:: yaml
:caption: Excerpt from EXT:dashboard/Configuration/Services.yaml
imports:
- { resource: Backend/DashboardWidgets.yaml }
.. note::
Note that you have to repeat all necessary information, e.g. :yaml:`services:` section with :yaml:`_defaults:` again.
.. code-block:: yaml
:caption: Excerpt from EXT:dashboard/Configuration/Backend/DashboardWidgets.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
TYPO3\CMS\Dashboard\Widgets\:
resource: '../Classes/Widgets/*'
dashboard.buttons.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems'
$link: 'https://news.typo3.com'
$target: '_blank'
dashboard.widget.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
arguments:
$buttonProvider: '@dashboard.buttons.t3news'
$options:
feedUrl: 'https://www.typo3.org/rss'
tags:
- name: dashboard.widget
identifier: 't3news'
groupNames: 'news'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description'
iconIdentifier: 'content-widget-rss'
height: 'large'
width: 'medium'
.. _register-new-widget-services-php:
Services.php File
-----------------
This is not intended for integrators but developers only, as this involves PHP experience.
The typical use case should be solved via :file:`Services.yaml`.
But for more complex situations, it is possible to register widgets via :file:`Services.php`.
Even if :file:`Services.php` contains PHP, it is only executed during compilation of the dependency injection container.
Therefore, it is not possible to check for runtime information like URLs, users, configuration or packages.
Instead, this approach can be used to register widgets only if their service dependencies are available.
The :php:`ContainerBuilder` instance provides a method :php:`hasDefinition()`
that may be used to check for optional dependencies.
Make sure to declare the optional dependencies in :file:`composer.json` as
suggested extensions to ensure packages are ordered correctly in order for
services to be registered with deterministic ordering.
The following example demonstrates how a widget can be registered via :file:`Services.php`:
.. code-block:: php
<?php
declare(strict_types=1);
namespace Vendor\ExtName;
use Vendor\ExtName\Widgets\ExampleWidget;
use Vendor\ExtName\Widgets\Provider\ExampleProvider;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use TYPO3\CMS\Report\Status;
return function (ContainerConfigurator $configurator, ContainerBuilder $containerBuilder) {
$services = $configurator->services();
if ($containerBuilder->hasDefinition(Status::class)) {
$services->set('widgets.dashboard.widget.exampleWidget')
->class(ExampleWidget::class)
->arg('$buttonProvider', new Reference(ExampleProvider::class))
->arg('$options', ['template' => 'Widget/ExampleWidget'])
->tag('dashboard.widget', [
'identifier' => 'widgets-exampleWidget',
'groupNames' => 'systemInfo',
'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:widgets.dashboard.widget.exampleWidget.title',
'description' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:widgets.dashboard.widget.exampleWidget.description',
'iconIdentifier' => 'content-widget-list',
'height' => 'medium',
'width' => 'medium'
])
;
}
};
Above example will register a new widget called ``widgets.dashboard.widget.exampleWidget``.
The widget is only registered, in case the extension "reports" is enabled, which
results in the availablity of the :php:`TYPO3\CMS\Report\Status` during container compile time.
Configuration is done in the same way as with :file:`Services.yaml`, except a PHP API is used.
The :php:`new Reference` equals to :yaml:`@` inside the YAML, to reference another service.
:yaml:`arguments:` are registered via :php:`->arg()` method call.
And :yaml:`tags:` are added via :php:`->tag()` method call.
Using this approach, it is possible to provide widgets that depend on 3rd party code,
without requiring this 3rd party code.
Instead the 3rd party code can be suggested and is supported if its installed.
Further information regarding how :file:`Services.php` works in general, can be found
at `symfony.com <https://symfony.com/doc/current/components/dependency_injection.html>`_.
Make sure to switch code examples from YAML to PHP.
@@ -0,0 +1,59 @@
.. include:: /Includes.rst.txt
.. _settings:
=====================================
Adjust settings of registered widgets
=====================================
.. versionadded:: 14.0
`Configurable Dashboard Widgets <https://docs.typo3.org/permalink/changelog:feature-107036-1738837673>`_
have been introduced with TYPO3 14.0.
.. contents:: Table of contents
.. _adjust-settings-of-widget-why:
.. _configurable-widgets:
Configurable dashboard widgets
------------------------------
.. versionadded:: 14.0
Dashboard widgets can be configured on a per-instance level using the Settings
API. This allows widget authors to define configurable settings that editors
can modify directly from the dashboard interface, making widgets more
flexible and user-friendly.
Examples are URLs for RSS feeds, limits on displayed items, or categories for
filtering content.
Each widget instance maintains its own configuration, enabling multiple
instances of the same widget type with different settings on the same or
different dashboards.
Configurable widgets display a `settings (cog) icon <https://docs.typo3.org/permalink/typo3/cms-dashboard:widgets-configuration>`_
and allow editors to configure the widget in a modal dialog.
Extension authors can implement :php-short:`\TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface`
to make their widgets configurable:
`Configurable dashboard widget implementation <https://docs.typo3.org/permalink/typo3/cms-dashboard:configurable-widget-implementation>`_.
.. _adjust-settings-of-widget:
Adjust settings of registered widgets
=====================================
Each widget is registered with an identifier, and all :file:`Services.*` files are merged.
Therefore it is possible to override widgets.
In order to override, the extension which should override has to be loaded after the extension that registered the widget.
Concrete options depend on the widget to configure.
Each widget should provide documentation covering all possible options and their meaning.
For delivered widgets by EXT:dashboard see :ref:`widgets`.
In case a widget defined by EXT:dashboard should be adjusted,
the extension has to define a dependency to EXT:dashboard.
Afterwards the widget can be registered again, with different options. See
:ref:`register-new-widget` to get an in depth example of how to register a widget.
@@ -0,0 +1,26 @@
.. include:: /Includes.rst.txt
.. _adjust-template-of-widget:
==========================
Adjust template of widgets
==========================
When adding own widgets, it might be necessary to provide custom templates.
In such a case the file path containing the template files needs to be added.
This is done using a :file:`Configuration/page.tsconfig` file, see
:doc:`changelog <ext_core:Changelog/12.0/Feature-96812-OverrideBackendTemplatesWithTSconfig>` and
:doc:`changelog <ext_core:Changelog/12.0/Feature-96614-AutomaticInclusionOfPageTsConfigOfExtensions>`
for details on this:
.. code-block:: typoscript
# Pattern: templates.typo3/cms-dashboard."something-unique" = "overriding-extension-composer-name":"entry-path"
templates.typo3/cms-dashboard.1644485473 = myvendor/myext:Resources/Private
A template file can then be added to path :file:`Resources/Private/Templates/Widgets/MyExtensionsGreatWidget.html`
and is referenced in the PHP class using :php:`->render('Widgets/MyExtensionsGreatWidget');`. The registration
into namespace :php:`typo3/cms-dashboard` is shared between all extensions. It is thus a good idea to give
template file names unique names (for instance by prefixing them with the extension name), to avoid situations
where templates from multiple extensions that provide different widgets override each other.
+111
View File
@@ -0,0 +1,111 @@
.. include:: /Includes.rst.txt
.. _adding-buttons:
=======================
Adding button to Widget
=======================
.. php:namespace:: TYPO3\CMS\Dashboard\Widgets
In order to add a button to a widget, a new dependency to an :php:`ButtonProviderInterface` can be added.
.. _adding-buttons-template:
Template
--------
The output itself is done inside of the Fluid template, for example :file:`Resources/Private/Templates/Widget/RssWidget.html`:
.. code-block:: html
<f:if condition="{button}">
<a href="{button.link}" target="{button.target}" class="widget-cta">
{f:translate(id: button.title, default: button.title)}
</a>
</f:if>
.. _adding-buttons-configuration:
Configuration
-------------
The configuration is done through an configured Instance of the dependency, for example :file:`Services.yaml`:
.. code-block:: yaml
services:
# …
dashboard.buttons.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
arguments:
$title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems'
$link: 'https://news.typo3.com'
$target: '_blank'
dashboard.widget.t3news:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
arguments:
# …
$buttonProvider: '@dashboard.buttons.t3news'
# …
See also: :php:`\TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider`.
.. confval:: $title
:type: string
:name: button-title
The title used for the button. E.g. an ``LLL:EXT:`` reference like
``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``.
.. confval:: $link
:type: string
:name: button-link
The link to use for the button. Clicking the button will open the link.
.. confval:: $target
:type: string
:name: button-target
The target of the link, e.g. ``_blank``.
``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``.
.. _adding-buttons-implementation:
Implementation
--------------
An example implementation could look like this:
.. code-block:: php
:caption: Classes/Widgets/RssWidget.php
class RssWidget implements WidgetInterface
{
public function __construct(
// …
private readonly ButtonProviderInterface $buttonProvider = null,
// …
) {
}
public function renderWidgetContent(): string
{
// …
$this->view->assignMultiple([
// …
'button' => $this->buttonProvider,
// …
]);
// …
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,34 @@
:navigation-title: Configurable widgets
.. include:: /Includes.rst.txt
.. _configurable-widget-implementation:
============================================
Configurable dashboard widget implementation
============================================
.. versionadded:: 14.0
`Configurable Dashboard Widgets <https://docs.typo3.org/permalink/changelog:feature-107036-1738837673>`_
have been introduced with TYPO3 14.0.
Widget authors can implement configurable widgets by using to the
renderer interface :php:`TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface`
which allows to defining settings in their widget renderer.
Settings are automatically validated and processed using the Settings API.
All types that are available for site settings definition are available:
`Definition types <https://docs.typo3.org/permalink/t3coreapi:definition-types>`_.
.. seealso::
:php:`TYPO3\CMS\Dashboard\Widgets\RssWidget` is a configurable widget
implementation.
.. _configurable-widget-implementation-example:
Example: A configurable widget implementation
=============================================
.. literalinclude:: _codesnippets/_ConfigurableWidget.php.inc
:language: php
:caption: EXT:my_extension/Classes/Widgets/ConfigurableWidget.php
@@ -0,0 +1,174 @@
.. include:: /Includes.rst.txt
.. _graph-widget-implementation:
======================
Implement graph widget
======================
.. php:namespace:: TYPO3\CMS\Dashboard\Widgets
First of all a new data provider is required, which will provide the data for the chart.
Next the data will be provided to the widget instance, which will be rendered with JavaScript modules and Css.
To make the dashboard aware of this workflow, some interfaces come together:
* :php:`EventDataInterface`
* :php:`AdditionalCssInterface`
Also the existing template file :file:`Widget/ChartWidget` is used, which provides necessary HTML to render the chart.
The provided ``eventData`` will be rendered as a chart and therefore has to match the expected structure.
An example would be :file:`Classes/Widgets/BarChartWidget.php`:
.. code-block:: php
:caption: Classes/Widgets/BarChartWidget.php
class BarChartWidget implements WidgetInterface, EventDataInterface, AdditionalCssInterface
{
public function __construct(
// …
private readonly ChartDataProviderInterface $dataProvider,
// …
) {
// …
$this->dataProvider = $dataProvider;
// …
}
public function renderWidgetContent(): string
{
// …
$this->view->assignMultiple([
// …
'configuration' => $this->configuration,
// …
]);
// …
}
public function getEventData(): array
{
return [
'graphConfig' => [
'type' => 'bar',
'options' => [
// …
],
'data' => $this->dataProvider->getChartData(),
],
];
}
public function getCssFiles(): array
{
return [];
}
public function getOptions(): array
{
return $this->options;
}
}
Together with :file:`Services.yaml`:
.. code-block:: yaml
services:
dashboard.widget.sysLogErrors:
class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget'
arguments:
# …
$dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider'
# …
tags:
- name: dashboard.widget
The configuration adds necessary CSS classes, as well as the ``dataProvider`` to use.
The provider implements :php:`ChartDataProviderInterface` and could look like the following.
.. code-block:: php
:caption: Classes/Widgets/Provider/SysLogErrorsDataProvider
class SysLogErrorsDataProvider implements ChartDataProviderInterface
{
/**
* Number of days to gather information for.
*
* @var int
*/
protected $days = 31;
/**
* @var array
*/
protected $labels = [];
/**
* @var array
*/
protected $data = [];
public function __construct(int $days = 31)
{
$this->days = $days;
}
public function getChartData(): array
{
$this->calculateDataForLastDays();
return [
'labels' => $this->labels,
'datasets' => [
[
'label' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.chart.dataSet.0'),
'backgroundColor' => WidgetApi::getDefaultChartColors()[0],
'border' => 0,
'data' => $this->data
]
]
];
}
protected function getNumberOfErrorsInPeriod(int $start, int $end): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
return (int)$queryBuilder
->count('*')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::ERROR, Connection::PARAM_INT)
),
$queryBuilder->expr()->gte(
'tstamp',
$queryBuilder->createNamedParameter($start, Connection::PARAM_INT)
),
$queryBuilder->expr()->lte(
'tstamp',
$queryBuilder->createNamedParameter($end, Connection::PARAM_INT)
)
)
->execute()
->fetchColumn();
}
protected function calculateDataForLastDays(): void
{
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d';
for ($daysBefore = $this->days; $daysBefore >= 0; $daysBefore--) {
$this->labels[] = date($format, strtotime('-' . $daysBefore . ' day'));
$startPeriod = strtotime('-' . $daysBefore . ' day 0:00:00');
$endPeriod = strtotime('-' . $daysBefore . ' day 23:59:59');
$this->data[] = $this->getNumberOfErrorsInPeriod($startPeriod, $endPeriod);
}
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+25
View File
@@ -0,0 +1,25 @@
.. include:: /Includes.rst.txt
.. _for-developer:
==============
For Developers
==============
Target group: **Developers**
Welcome to our small dashboard introduction.
We will explain how to create widget groups and implement widgets.
.. include:: /Shared/DifferenceRegistrationAndImplementation.rst.txt
.. toctree::
:maxdepth: 3
:titlesonly:
WidgetImplementation
ConfigurableWidgets
MakeRefreshable
AddingButtons
GraphWidgetImplementation
Interfaces
+178
View File
@@ -0,0 +1,178 @@
.. include:: /Includes.rst.txt
.. _interfaces:
==========
Interfaces
==========
The following list provides information for all necessary interfaces that are used inside of this documentation.
For up to date information, please check the source code.
.. php:namespace:: TYPO3\CMS\Dashboard\Widgets
.. php:class:: WidgetInterface
Has to be implemented by all widgets.
This interface defines public API used by ext:dashboard to interact with widgets.
.. php:method:: renderWidgetContent()
:returntype: string
:returns: The rendered HTML to display.
.. php:method:: getOptions()
:returntype: array
:returns: The options of the widget as set in the registration.
.. php:class:: RequestAwareWidgetInterface
This interface declares a widget has a dependency to the current PSR-7 request.
When implemented, the dashboard controller will call :php:`setRequest()` immediately
after widget instantiation to hand over the current request. Widgets that rely on
:php:`BackendViewFactory` typically need the current request.
.. php:method:: setRequest(ServerRequestInterface $request)
:returntype: void
.. php:class:: WidgetConfigurationInterface
Used internally in ext:dashboard.
Used to separate internal configuration from widgets.
Can be required in widget classes and passed to view.
.. php:method:: getIdentifier()
:returntype: string
:returns: Unique identifer of a widget.
.. php:method:: getServiceName()
:returntype: string
:returns: Service name providing the widget implementation.
.. php:method:: getGroupNames()
:returntype: array
:returns: Group names associated to this widget.
.. php:method:: getTitle()
:returntype: string
:returns: Title of a widget, this is used for the widget selector.
.. php:method:: getDescription()
:returntype: string
:returns: Description of a widget, this is used for the widget selector.
.. php:method:: getIconIdentifier()
:returntype: string
:returns: Icon identifier of a widget, this is used for the widget selector.
.. php:method:: getHeight()
:returntype: int
:returns: Height of a widget in rows (1-6).
.. php:method:: getWidth()
:returntype: int
:returns: Width of a widget in columns (1-4).
.. php:class:: AdditionalJavaScriptInterface
Widgets implementing this interface will add the provided JavaScript files.
Those files will be loaded in dashboard view if the widget is added at least once.
.. php:method:: getJsFiles()
Returns a list of JavaScript file names that should be included, e.g.:
.. code-block:: php
return [
'EXT:my_extension/Resources/Public/JavaScript/file.js',
'EXT:my_extension/Resources/Public/JavaScript/file2.js',
];
:returntype: array
:returns: List of JS files to load.
.. php:class:: AdditionalCssInterface
Widgets implementing this interface will add the provided Css files.
Those files will be loaded in dashboard view if the widget is added at least once.
.. php:method:: getCssFiles()
Returns a list of Css file names that should be included, e.g.:
.. code-block:: php
return [
'EXT:my_extension/Resources/Public/Css/widgets.css',
'EXT:my_extension/Resources/Public/Css/list-widget.css',
];
:returntype: array
:returns: List of Css files to load.
.. php:class:: ButtonProviderInterface
.. php:method:: getTitle()
:returntype: string
:returns: The title used for the button. E.g. an ``LLL:EXT:`` reference like
``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``.
.. php:method:: getLink()
:returntype: string
:returns: The link to use for the button. Clicking the button will open the link.
.. php:method:: getTarget()
:returntype: string
:returns: The target of the link, e.g. ``_blank``.
``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``.
.. php:class:: NumberWithIconDataProviderInterface
.. php:method:: getNumber()
:returntype: integer
:returns: The number to display for an number widget.
.. php:class:: EventDataInterface
.. php:method:: getEventData()
:returntype: array
:returns: Returns data which should be send to the widget as JSON encoded value.
.. php:class:: ChartDataProviderInterface
.. php:method:: getChartData()
:returntype: array
:returns: Provide the data for a graph.
The data and options you have depend on the type of chart.
More information can be found in the documentation of the specific type:
Bar
https://www.chartjs.org/docs/latest/charts/bar.html#data-structure
Doughnut
https://www.chartjs.org/docs/latest/charts/doughnut.html#data-structure
.. php:class:: ListDataProviderInterface
.. php:method:: getItems()
:returntype: array
:returns: Provide the array if items.
Each entry should be a single string.
@@ -0,0 +1,83 @@
.. include:: /Includes.rst.txt
.. _make-refreshable:
==================
The refresh option
==================
In each widget the refresh option can be enabled. If the option is enabled the
widget displays a reload button in the top right corner. It can then be
refreshed via user interaction or via a javascript api.
To enable the refresh action button, you have to define the
:yaml:`refreshAvailable` option in the :yaml:`$options` part of the widget
registration. Below is an example of a RSS widget with the refresh option enabled.
.. code-block:: yaml
dashboard.widget.myOwnRSSWidget:
class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
arguments:
$options:
rssFile: 'https://typo3.org/rss'
lifeTime: 43200
refreshAvailable: true
tags:
- name: dashboard.widget
identifier: 'myOwnRSSWidget'
groupNames: 'general'
title: 'LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.title'
description: 'LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.description'
iconIdentifier: 'content-widget-rss'
height: 'medium'
width: 'medium'
.. note::
In this example, the TYPO3 core :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget`
widget class is used. In case you have implemented own widget classes, you
have to implement the :php:`getOptions()` method, returning :php:`$this->options`,
to the corresponding classes. Otherwise the refresh option won't have any
effect.
.. _refresh-button:
Enable the refresh button
-------------------------
Widgets can render a refresh button to allow users to manually refresh them.
This is done by passing the value :php:`['refreshAvailable'] = true;` back
via :php:`getOptions()` method of the widget.
All TYPO3 Core widgets implement this behaviour and allow integrators to
configure the option:
.. include:: /Widgets/Options/RefreshAvailable.rst.txt
.. _refresh-javascript:
JavaScript API
--------------
It is possible for all widgets to dispatch an event, which will cause
the widget being refreshed. This is possible for all widgets on the dashboard
even when the :yaml:`refreshAvailable` option is not defined, or set to `false`.
This will give developers the option to refresh the widgets whenever they think
it is appropriate.
To refresh a widget, dispatch the :js:`widgetRefresh` event on the
widget container (the :html:`div` element with the :html:`dashboard-item` class).
You can identify the container by the data attribute :html:`widget-hash`, which
is a unique hash for every widget, even if you have more widgets of the same
type on your dashboard.
A small example below:
.. code-block:: javascript
document
.querySelector('[data-widget-hash="{your-unique-widget-hash}"]')
.dispatchEvent(new Event('widgetRefresh', {bubbles: true}));
See :ref:`implement-new-widget-custom-js` to learn how to add custom JavaScript.
@@ -0,0 +1,195 @@
.. include:: /Includes.rst.txt
.. _implement-new-widget:
====================
Implement new widget
====================
.. versionadded:: 14.0
`Configurable Dashboard Widgets <https://docs.typo3.org/permalink/changelog:feature-107036-1738837673>`_
have been introduced with TYPO3 14.0.
See also
.. php:namespace:: TYPO3\CMS\Dashboard\Widgets
.. seealso::
For information regarding registration of widgets, see: :ref:`register-new-widget`.
This section describes the implementation of new widgets for developers.
Each extension can provide multiple Widgets.
ext:dashboard already ships with some widget implementations.
Each widget has to be implemented as a PHP class.
The PHP class defines the concrete implementation and features of a widget,
while registration adds necessary options for a concrete instance of a widget.
For example a TYPO3.org RSS Widget would consist of an :php:`RssWidget` PHP class.
This class would provide the implementation to fetch rss news and display them.
The concrete registration will provide the URL to RSS feed.
.. _widget-php-class:
PHP class
---------
Each Widget has to be a PHP class.
This class has to implement the :php:`WidgetInterface` and could look like this:
.. code-block:: php
class RssWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly Cache $cache,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = []
) {
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'items' => $this->getRssItems(),
'options' => $this->options,
'button' => $this->getButton(),
'configuration' => $this->configuration,
]);
return $view->render('Widget/RssWidget');
}
protected function getRssItems(): array
{
$items = [];
// Logic to populate $items array
return $items;
}
public function getOptions(): array
{
return $this->options;
}
}
The class should always provide documentation how to use in :file:`Services.yaml`.
The above class is documented at :ref:`rss-widget`.
The documentation should provide all possible options and an concrete example.
It should make it possible for integrators to register new widgets using the implementation.
The difference between :php:`$options` and :php:`$configuration` in above example is the following:
:php:`$options` are the options for this implementation which can be provided through :file:`Services.yaml`.
:php:`$configuration` is an instance of :php:`WidgetConfigurationInterface`
holding all internal configuration, like icon identifier.
.. _implement-new-widget-fluid:
Using Fluid
-----------
Most widgets will need a template.
Therefore each widget can define :php:`BackendViewFactory` as requirement for DI in
constructor, like done in RSS example.
.. _implement-new-widget-custom-js:
Providing custom JS
-------------------
There are two ways to add JavaScript for an widget:
JavaScript module
Implement :php:`\TYPO3\CMS\Dashboard\Widgets\JavaScriptInterface`:
.. code-block:: php
class ExampleChartWidget implements JavaScriptInterface
{
// ...
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create(
'@myvendor/my-extension/module-name.js'
)->invoke('initialize'),
JavaScriptModuleInstruction::create(
'@myvendor/my-extension/module-name2.js'
)->invoke('initialize'),
];
}
}
.. seealso::
:ref:`t3coreapi:backend-javascript-es6` for more info about JavaScript in TYPO3 Backend.
Plain JS files
Implement :php:`AdditionalJavaScriptInterface`:
.. code-block:: php
class RssWidget implements WidgetInterface, AdditionalJavaScriptInterface
{
public function getJsFiles(): array
{
return [
'EXT:my_extension/Resources/Public/JavaScript/file.js',
'EXT:my_extension/Resources/Public/JavaScript/file2.js',
];
}
}
JavaScript
Implement :php:`\TYPO3\CMS\Dashboard\Widgets\JavaScriptInterface`:
.. code-block:: php
class ExampleChartWidget implements JavaScriptInterface
{
// ...
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create(
'@typo3/dashboard/chart-initializer.js'
)->invoke('initialize'),
];
}
}
All ways can be combined.
.. _custom-css:
Providing custom CSS
--------------------
It is possible to add custom Css to style widgets.
Implement :php:`AdditionalCssInterface`:
.. code-block:: php
class RssWidget implements WidgetInterface, AdditionalCssInterface
{
public function getCssFiles(): array
{
return [
'EXT:my_extension/Resources/Public/Css/widgets.css',
'EXT:my_extension/Resources/Public/Css/list-widget.css',
];
}
}
@@ -0,0 +1,43 @@
<?php
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetResult;
use TYPO3\CMS\Core\Settings\SettingDefinition;
class ConfigurableWidget implements WidgetRendererInterface
{
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'title',
type: 'string',
default: 'Default Title',
label: 'LLL:EXT:my_extension/Resources/Private/Language/locallang_my_widget.xlf:settings.label',
description: 'LLL:EXT:my_extension/Resources/Private/Language/locallang_my_widget.xlf:settings.description.label',
),
new SettingDefinition(
key: 'limit',
type: 'int',
default: 10,
label: 'LLL:EXT:my_extension/Resources/Private/Language/locallang_my_widget.xlf:settings.limit',
description: 'LLL:EXT:my_extension/Resources/Private/Language/locallang_my_widget.xlf:settings.description.limit',
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$settings = $context->settings;
$title = $settings->get('title');
$limit = $settings->get('limit');
// Use settings to customize widget output
return new WidgetResult(
content: '<!-- widget content -->',
label: $title,
refreshable: true
);
}
}
+167
View File
@@ -0,0 +1,167 @@
.. include:: /Includes.rst.txt
.. _for-editors:
===========
For Editors
===========
Target group: **Editors**
Welcome to our small dashboard introduction.
We will explain the basic usage of the TYPO3 dashboard.
.. _opening-dashboard:
Opening Dashboard
=================
By default the dashboard is opened when logging into the backend.
The dashboard can be opened at any time by clicking the entry
:guilabel:`Dashboard` in the module menu.
.. figure:: /Images/DashboardPosition.png
:align: center
Open the dashboard by clicking on :guilabel:`Dashboard`.
.. note::
If the entry :guilabel:`Dashboard` is not visible in the menu there are two
possible causes:
* You lack sufficient rights to view the dashboard.
* The system extension `dashboard` was not installed on your system.
Ask your administrator about this.
.. _adding-dashboard:
Adding Dashboard
================
The EXT:dashboard allows to have multiple dashboards.
Switching between different dashboards is possible by using the corresponding tab.
In order to add further dashboards, press the :guilabel:`+` sign.
.. figure:: /Images/DashboardTabs.png
:align: center
Tabs allowing to switch and add dashboards.
A wizard should open which allows to add the new dashboard.
There you can select a preset. At least the default preset, which is shipped
by core should be available. Depending on system configuration further dashboard
presets might be available.
.. figure:: /Images/DashboardWizard.png
:align: center
Wizard to add a new dashboard.
.. _editing-dashboard:
Editing Dashboard
=================
Existing dashboards can be edited and deleted.
On the right side of the tab bar are the icons which allow deletion and adjusting
settings of the currently active dashboard.
.. figure:: /Images/DashboardTabs.png
:align: center
Icons on the right side of the tab bar allow adjusting settings or deletion of
the currently selected dashboard.
.. _adding-widgets:
Adding Widgets
==============
Widgets can be added to a dashboard.
Dashboards which do not contain any widget yet, offer a dialog in the middle of
the screen, which allows to add one or more widgets to the current dashboard.
All dashboards allow to add further widgets in the lower right corner through the
:guilabel:`+` Icon.
.. figure:: /Images/AddWidget.png
:align: center
Empty dashboard with possibilities to add new widgets.
Once the action to add a new widget was triggered, a wizard opens which allows to
select the widget to add.
Widgets are grouped in tabs and can be added by clicking on them.
.. figure:: /Images/WidgetWizard.png
:align: center
Wizard to select a new widget that will be added to the active dashboard.
.. _widgets-configuration:
Widget configuration
====================
.. versionadded:: 14.0
`Configurable Dashboard Widgets <https://docs.typo3.org/permalink/changelog:feature-107036-1738837673>`_
have been introduced with TYPO3 14.0.
* Dashboard widgets display a settings (cog) icon when they support configuration
* Clicking the settings icon opens a modal dialog with configurable options
* Settings are applied immediately after saving, with the widget content
refreshing automatically
* Each widget can be configured independently per user / per instance
.. figure:: /Images/DashboardConfiguration.png
:alt: Screenshot of the dashboard widget "RSS Feed" with the location of the settings (cog) icon
Click the settings (cog) icon to configure a feed
Extension authors can make their widgets configurable:
`Configurable dashboard widget implementation <https://docs.typo3.org/permalink/typo3/cms-dashboard:configurable-widget-implementation>`_.
.. _moving-widgets:
Moving Widgets
==============
Widgets can be moved around. Therefore a widget needs to be hovered.
If a widget is hovered some icons appear in the upper right corner of the widget.
To move the widget, click and hold left mouse button on the cross icon.
Then move to the target position.
.. figure:: /Images/WidgetMove.png
:align: center
Widget in hover mode with additional icons in upper right corner.
.. _deleting-widgets:
Deleting Widgets
================
To delete a widget, the widget needs to be hovered.
If a widget is hovered some icons appear in the upper right corner of the widget.
Click the trash icon which appears to delete the widget.
.. figure:: /Images/WidgetMove.png
:align: center
Widget in hover mode with additional icons in upper right corner.
In order to prevent accidentally deletion, a modal is shown to confirm deletion.
Confirm by clicking the :guilabel:`Remove` button.
.. figure:: /Images/WidgetDelete.png
:align: center
Modal to confirm deletion of widget.
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+1
View File
@@ -0,0 +1 @@
.. You can put central messages to display on all pages here
+55
View File
@@ -0,0 +1,55 @@
.. include:: /Includes.rst.txt
.. _start:
===============
TYPO3 Dashboard
===============
:Extension key:
dashboard
:Package name:
typo3/cms-dashboard
:Version:
|release|
:Language:
en
:Author:
TYPO3 contributors
:License:
This document is published under the
`Open Content License <https://www.openhub.net/licenses/opl>`__.
:Rendered:
|today|
----
This TYPO3 backend module is used to configure and create backend widgets.
----
**Table of Contents:**
.. toctree::
:maxdepth: 2
:titlesonly:
Introduction/Index
Installation/Index
Editor/Index
Configuration/Index
Developer/Index
Widgets/Index
.. Meta Menu
.. toctree::
:hidden:
Sitemap
+54
View File
@@ -0,0 +1,54 @@
.. include:: /Includes.rst.txt
.. _installation:
============
Installation
============
Target group: **Administrators**
This extension is part of the TYPO3 Core, but not installed by default.
.. contents:: Table of contents
:local:
.. _installation-composer:
Installation with Composer
==========================
Check whether you are already using the extension with:
.. code-block:: bash
composer show | grep dashboard
This should either give you no result or something similar to:
.. code-block:: none
typo3/cms-dashboard v12.4.11
If it is not installed yet, use the ``composer require`` command to install
the extension:
.. code-block:: bash
composer require typo3/cms-dashboard
The given version depends on the version of the TYPO3 Core you are using.
.. _installation-no-composer:
Installation without Composer
=============================
In an installation without Composer, the extension is already shipped. You just have to activate it.
Head over to the extension manager and activate the extension.
.. figure:: /Images/InstallActivate.png
:class: with-shadow
:alt: Extension manager showing Dashboard extension
Extension manager showing Dashboard extension
+16
View File
@@ -0,0 +1,16 @@
:navigation-title: Introduction
.. include:: /Includes.rst.txt
.. _introduction:
==============================
Introduction: What does it do?
==============================
This extension provides a new TYPO3 backend module "Dashboard".
Users can create multiple dashboards visible in this module, and switch between those
dashboards.
Each of the dashboards can have multiple widgets.
Developers are able to create new widgets.
Integrators and developers are able to register new widgets through configuration.
@@ -0,0 +1,13 @@
.. note::
Difference between **registration** of widgets and **implementation** of widgets:
Widgets provide some functionality, e.g. collect system log errors over a time span.
This functionality is provided by the implementation, a PHP class with some code.
The registration is done in :file:`Services.yaml`,
in order to create the actual widget with provided functionality.
During registration options can be set, e.g. the time span.
Registration is documented at :ref:`register-new-widget`,
while implementation is documented at
:ref:`implement-new-widget`.
+11
View File
@@ -0,0 +1,11 @@
:template: sitemap.html
.. include:: /Includes.rst.txt
.. _sitemap:
=======
Sitemap
=======
.. The sitemap.html template will insert here the page tree automatically.
+73
View File
@@ -0,0 +1,73 @@
.. include:: /Includes.rst.txt
.. _bar-chart-widget:
================
Bar Chart Widget
================
.. php:namespace:: TYPO3\CMS\Dashboard\Widgets
Widgets using this class will show a bar chart with the provided data.
This kind of widgets are useful if you want to show some statistics of for example
historical data.
.. php:class:: TYPO3\CMS\Dashboard\Widgets\BarChartWidget
.. _bar-chart-widget-example:
Example
-------
.. code-block:: yaml
:caption: Excerpt from EXT:dashboard/Configuration/Services.yaml
services:
dashboard.widget.sysLogErrors:
class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget'
arguments:
$dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider'
$buttonProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider'
$options:
refreshAvailable: true
tags:
- name: dashboard.widget
identifier: 'sysLogErrors'
groupNames: 'systemInfo'
title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.title'
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.description'
iconIdentifier: 'content-widget-chart-bar'
height: 'medium'
width: 'medium'
.. _bar-chart-widget-options:
Options
-------
.. include:: Options/RefreshAvailable.rst.txt
.. _bar-chart-widget-dependencies:
Dependencies
------------
.. confval:: $dataProvider
:type: :php:`\TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface`
:name: bar-chart-widget-dataProvider
To add data to a Bar Chart widget, you need to have a DataProvider that implements
the interface :php-short:`\TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface`.
See :ref:`graph-widget-implementation` for further information.
.. confval:: $buttonProvider
:type: :php:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface`
:name: bar-chart-widget-buttonProvider
Optionally you can add a button with a link to some additional data.
This button should be provided by a ButtonProvider that implements the interface
:php-short:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface`.
See :ref:`adding-buttons` for further info and configuration options.

Some files were not shown because too many files have changed in this diff Show More