-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FEATURE] Add content changes widget
Adds a new widget which shows: * Pending publishing state change, if any. Lists records which have starttime / endtime in the future. * Changed records along with name of user who made the change. Attempts to select a total of $limit (default 5) records while distributing the number of records equally between the "pending" and "changed" sets, giving preference to the "pending" set: * If limit is 5 and 3 "pending" were selected, only 2 "changed" will be selected. * If limit is 5 and no "pending" were selected, all 5 are selected from the "changed" set. If there are no records in a group the table containing the record data is not shown. if there are no results in either of the two groups, a "nothing to show" message is shown instead.
- Loading branch information
1 parent
5eecce3
commit a3c2bba
Showing
4 changed files
with
250 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
<?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); | ||
} | ||
|
||
return $results; | ||
} | ||
|
||
protected function getIconFactory(): IconFactory | ||
{ | ||
return GeneralUtility::makeInstance(IconFactory::class); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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="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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters