Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add content changes widget #69

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions Classes/Widgets/ContentChangesWidget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);

namespace FriendsOfTYPO3\Dashboard\Widgets;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ContentChangesWidget extends AbstractListWidget
{
/**
* @var string
*/
protected $templateName = 'contentChanges';

public function __construct()
{
AbstractListWidget::__construct();
$this->width = 4;
$this->height = 4;
$this->title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.contentchanges.title';
$this->description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.contentchanges.description';
$this->iconIdentifier = 'dashboard-typo3';
}

public function prepareData(): void
{
$this->items['pendingRecords'] = $this->getPublishStateChangePendingRecords();
$this->items['changedRecords'] = $this->getMostRecentChangedRecords(count($this->items['pendingRecords']));
}

protected function getPublishStateChangePendingRecords(): iterable
{
$limit = ceil($this->limit / 2);
$pendingRecords = $this->getPublishStateChangePendingRecordsFromTable('pages') + $this->getPublishStateChangePendingRecordsFromTable('tt_content');
usort($pendingRecords, function (array $a, array $b) {
return min($b['starttime'], $b['endtime']) <=> min($a['starttime'], $a['endtime']);
});
return count($pendingRecords) > $limit ? array_slice($pendingRecords, 0, $limit) : $pendingRecords;
}

protected function getMostRecentChangedRecords(int $alreadyCollectedItemCount): iterable
{
$limit = $this->limit - $alreadyCollectedItemCount;
$changedRecords = $this->getMostRecentChangedRecordsFromTable('pages') + $this->getMostRecentChangedRecordsFromTable('tt_content');
usort($changedRecords, function (array $a, array $b) {
return $b['tstamp'] <=> $a['tstamp'];
});
return count($changedRecords) > $limit ? array_slice($changedRecords, 0, $limit) : $changedRecords;
}

protected function getPublishStateChangePendingRecordsFromTable(string $table): array
{
$labelFieldsForQuery = $this->resolveLabelFieldsForTable($table);
$now = time();

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$queryBuilder->getRestrictions()->removeByType(StartTimeRestriction::class)->removeByType(EndTimeRestriction::class);
$query = $queryBuilder->select(...$labelFieldsForQuery)
->from($table, 't')
->orWhere(
$queryBuilder->expr()->gte('t.starttime', $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)),
$queryBuilder->expr()->gte('t.endtime', $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT))
)->setMaxResults($this->limit);

return $this->processItemsQuery($table, $query);
}

protected function getMostRecentChangedRecordsFromTable(string $table): array
{
$labelFieldsForQuery = $this->resolveLabelFieldsForTable($table);
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$query = $queryBuilder->select('u.username', ...$labelFieldsForQuery)
->from($table, 't')
->rightJoin(
't',
'sys_log',
'l',
(string) $queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('l.recuid', 't.uid'),
$queryBuilder->expr()->eq('l.tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR))
)
)
->leftJoin(
'l',
'be_users',
'u',
'u.uid = l.userid'
)
->orderBy('t.tstamp', 'DESC')
->groupBy('t.uid', 't.pid', 't.sys_language_uid', 'u.username', ...$labelFieldsForQuery)
->setMaxResults($this->limit);

return $this->processItemsQuery($table, $query);
}

protected function resolveLabelFieldsForTable(string $table): array
{
$labelFields = [$GLOBALS['TCA'][$table]['ctrl']['label']] + explode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'] ?? '');
$labelFields = array_filter($labelFields);
$labelFieldsForQuery = [];
foreach ($labelFields as $labelField) {
$labelFieldsForQuery[] = 't.' . $labelField;
}
return $labelFieldsForQuery;
}

protected function processItemsQuery(string $table, QueryBuilder $query): array
{
$iconFactory = $this->getIconFactory();

$query->addSelect('t.uid', 't.pid', 't.tstamp', 't.sys_language_uid', 't.starttime', 't.endtime');
if ($table === 'pages') {
$query->addSelect('t.doktype', 't.is_siteroot');
} elseif ($table === 'tt_content') {
$query->addSelect('t.CType');
}

$results = $query->execute()->fetchAll();
if (!$results) {
return [];
}
foreach ($results as &$result) {
$result['type'] = $table;
$result['label'] = BackendUtility::getRecordTitle($table, $result);
$result['icon'] = $iconFactory->getIconForRecord($table, $result, Icon::SIZE_SMALL);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure if we want coloured icons in the dashboard. We want to keep the dashboard simple and no overdue of color. On the other hand it might be a recognisable icon for the user.

@irenesacchi what do you think?

}

return $results;
}

protected function getIconFactory(): IconFactory
{
return GeneralUtility::makeInstance(IconFactory::class);
}
}
34 changes: 34 additions & 0 deletions Resources/Private/Language/locallang.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@
<source>This overview is showing all (%s) pages without description</source>
</trans-unit>

<trans-unit id="widgets.contentchanges.title" xml:space="preserve">
<source>Content Changes</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.description" xml:space="preserve">
<source>Shows most recently edited pages/content and up-coming publishing and un-publishing based on start/end time</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.label.changed" xml:space="preserve">
<source>Changed item</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.label.pending" xml:space="preserve">
<source>Pending publish/unpublish</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.type" xml:space="preserve">
<source>Type</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.date" xml:space="preserve">
<source>Date</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.user" xml:space="preserve">
<source>Edit by</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.starts" xml:space="preserve">
<source>Publishes on</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.ends" xml:space="preserve">
<source>Unpublishes on</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.id" xml:space="preserve">
<source>ID</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.nothing_to_show" xml:space="preserve">
<source>Nothing to show</source>
</trans-unit>

<trans-unit id="widgets.lastLogins.title" xml:space="preserve">
<source>Last 5 logins</source>
</trans-unit>
Expand Down
70 changes: 70 additions & 0 deletions Resources/Private/Templates/Widget/ContentChanges.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:backend="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers" xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Widget/WidgetWithTitle" />

<f:section name="main">
<f:if condition="!{items.changedRecords} && !{items.pendingRecords}">
<h4><f:translate key="widgets.contentchanges.nothing_to_show" extensionName="dashboard" /></h4>
</f:if>
<f:if condition="{items.pendingRecords}">
<table class="widget-table">
<thead>
<tr>
<th><f:translate key="widgets.contentchanges.label.pending" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.type" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.id" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.starts" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.ends" extensionName="dashboard" /></th>
<th></th>
</tr>
</thead>
<tbody>
<f:for each="{items.pendingRecords}" as="item">
<tr>
<td>{item.icon | f:format.raw()} {item.label}</td>
<td>{item.type}</td>
<td>{item.uid}</td>
<td><f:if condition="{item.starttime}"><f:format.date format="%d-%m-%Y %H:%M">{item.starttime}</f:format.date></f:if></td>
<td><f:if condition="{item.endtime}"><f:format.date format="%d-%m-%Y %H:%M">{item.endtime}</f:format.date></f:if></td>
<td class="widget-edit">
<backend:link.editRecord uid="{item.uid}" table="{item.type}" returnUrl="{f:be.uri(route: 'dashboard')}">
<core:icon identifier="actions-open" alternativeMarkupIdentifier="inline" />
</backend:link.editRecord>
</td>
</tr>
</f:for>
</tbody>
</table>
</f:if>
<f:if condition="{items.changedRecords}">
<table class="widget-table">
<thead>
<tr>
<th><f:translate key="widgets.contentchanges.label.changed" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.type" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.id" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.date" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.user" extensionName="dashboard" /></th>
<th></th>
</tr>
</thead>
<tbody>
<f:for each="{items.changedRecords}" as="item">
<tr>
<td>{item.icon | f:format.raw()} {item.label}</td>
<td>{item.type}</td>
<td>{item.uid}</td>
<td><f:format.date format="%d-%m-%Y %H:%M">{item.tstamp}</f:format.date></td>
<td>{item.username}</td>
<td class="widget-edit">
<backend:link.editRecord uid="{item.uid}" table="{item.type}" returnUrl="{f:be.uri(route: 'dashboard')}">
<core:icon identifier="actions-open" alternativeMarkupIdentifier="inline" />
</backend:link.editRecord>
</td>
</tr>
</f:for>
</tbody>
</table>
</f:if>
</f:section>

</html>
1 change: 1 addition & 0 deletions ext_localconf.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
$widgetRegistry->registerWidget('sysLogErrors', \FriendsOfTYPO3\Dashboard\Widgets\SysLogErrorsWidget::class);
$widgetRegistry->registerWidget('t3News', \FriendsOfTYPO3\Dashboard\Widgets\T3NewsWidget::class);
$widgetRegistry->registerWidget('documentation', \FriendsOfTYPO3\Dashboard\Widgets\DocumentationWidget::class);
$widgetRegistry->registerWidget('contentChanges', \FriendsOfTYPO3\Dashboard\Widgets\ContentChangesWidget::class);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration of widgets changed. Can you merge master into your branch and do some little refactoring in the registration?


$dashboardRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\FriendsOfTYPO3\Dashboard\Registry\DashboardRegistry::class);
$dashboardRegistry->registerDashboard('default', 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default');
Expand Down