Skip to content

Commit

Permalink
[BUGFIX] Allow proper back-linking in File List
Browse files Browse the repository at this point in the history
The File List now creates links through
the UriBuilder, allowing to use actions (such as rename)
while then keeping search parameter, or pagination parameters
properly.

In addition, some code is now cleaned up and streamlined (e.g.
the styling of the buttons in the "multi-clipboard mode" is now correct).

Resolves: #94506
Releases: master
Change-Id: I0cf7dde9e94738b124818de724ec5e47dc1a8ac0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/69761
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Jochen <rothjochen@gmail.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Jochen <rothjochen@gmail.com>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack committed Jul 8, 2021
1 parent 9ea6ad0 commit 1eda452
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 71 deletions.
4 changes: 4 additions & 0 deletions Build/Sources/Sass/typo3/_element_buttons.scss
Expand Up @@ -44,6 +44,10 @@
}
}

label.btn-checkbox {
margin-bottom: 0;
}

.btn-clear {
&:focus {
outline: 1px dotted #000;
Expand Down
Expand Up @@ -19,10 +19,13 @@ import broadcastService = require('TYPO3/CMS/Backend/BroadcastService');
import Tooltip = require('TYPO3/CMS/Backend/Tooltip');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');

type QueryParameters = {[key: string]: string};

enum Selectors {
fileListFormSelector = 'form[name="fileListForm"]',
commandSelector = 'input[name="cmd"]',
searchFieldSelector = 'input[name="searchTerm"]'
searchFieldSelector = 'input[name="searchTerm"]',
pointerFieldSelector = 'input[name="pointer"]'
}

/**
Expand All @@ -33,6 +36,7 @@ class Filelist {
private fileListForm: HTMLFormElement = document.querySelector(Selectors.fileListFormSelector);
private command: HTMLInputElement = this.fileListForm.querySelector(Selectors.commandSelector)
private searchField: HTMLInputElement = this.fileListForm.querySelector(Selectors.searchFieldSelector);
private pointerField: HTMLInputElement = this.fileListForm.querySelector(Selectors.pointerFieldSelector);
private activeSearch: boolean = (this.searchField.value !== '');

protected static openInfoPopup(type: string, identifier: string): void {
Expand Down Expand Up @@ -79,6 +83,18 @@ class Filelist {
broadcastService.post(message);
}

private static parseQueryParameters (location: Location): QueryParameters {
let queryParameters: QueryParameters = {};
if (location && Object.prototype.hasOwnProperty.call(location, 'search')) {
let parameters = location.search.substr(1).split('&');
for (let i = 0; i < parameters.length; i++) {
const parameter = parameters[i].split('=');
queryParameters[decodeURIComponent(parameter[0])] = decodeURIComponent(parameter[1]);
}
}
return queryParameters;
}

constructor() {
Filelist.processTriggers();
DocumentService.ready().then((): void => {
Expand Down Expand Up @@ -139,6 +155,15 @@ class Filelist {

private submitClipboardFormWithCommand(cmd: string): void {
this.command.value = cmd;
// In case we just copy elements to the clipboard, we try to fetch a possible pointer from the query
// parameters, so after the form submit, we get to the same view as before. This is not done for delete
// commands, since this may lead to empty sites, in case all elements from the current site are deleted.
if (cmd === 'setCB') {
const pointerValue: string = Filelist.parseQueryParameters(document.location).pointer;
if (pointerValue) {
this.pointerField.value = pointerValue;
}
}
this.fileListForm.submit();
}
}
Expand Down
2 changes: 1 addition & 1 deletion typo3/sysext/backend/Resources/Public/Css/backend.css

Large diffs are not rendered by default.

Expand Up @@ -64,6 +64,7 @@ class FileListController implements LoggerAwareInterface
protected string $id = '';
protected string $cmd = '';
protected string $searchTerm = '';
protected int $pointer = 0;
protected ?Folder $folderObject = null;
protected ?DuplicationBehavior $overwriteExistingFiles = null;

Expand Down Expand Up @@ -108,6 +109,7 @@ public function handleRequest(ServerRequestInterface $request): ResponseInterfac
$this->id = (string)($parsedBody['id'] ?? $queryParams['id'] ?? '');
$this->cmd = (string)($parsedBody['cmd'] ?? $queryParams['cmd'] ?? '');
$this->searchTerm = (string)($parsedBody['searchTerm'] ?? $queryParams['searchTerm'] ?? '');
$this->pointer = (int)($request->getParsedBody()['pointer'] ?? $request->getQueryParams()['pointer'] ?? 0);
$this->overwriteExistingFiles = DuplicationBehavior::cast(
$parsedBody['overwriteExistingFiles'] ?? $queryParams['overwriteExistingFiles'] ?? null
);
Expand Down Expand Up @@ -364,7 +366,7 @@ protected function initializeFileList(ServerRequestInterface $request): void
// Start up the file list by including processed settings.
$this->filelist->start(
$this->folderObject,
MathUtility::forceIntegerInRange((int)($request->getParsedBody()['pointer'] ?? $request->getQueryParams()['pointer'] ?? 0), 0, 100000),
MathUtility::forceIntegerInRange($this->pointer, 0, 100000),
(string)($this->MOD_SETTINGS['sort'] ?? ''),
(bool)($this->MOD_SETTINGS['reverse'] ?? false),
(bool)($this->MOD_SETTINGS['clipBoard'] ?? false)
Expand Down Expand Up @@ -427,7 +429,10 @@ protected function registerFileListCheckboxes(): void
$addParams = '';

if ($this->searchTerm) {
$addParams = '&searchTerm=' . htmlspecialchars($this->searchTerm);
$addParams .= '&searchTerm=' . htmlspecialchars($this->searchTerm);
}
if ($this->pointer) {
$addParams .= '&pointer=' . $this->pointer;
}

$this->view->assign('checkboxes', [
Expand Down
105 changes: 39 additions & 66 deletions typo3/sysext/filelist/Classes/FileList.php
Expand Up @@ -122,13 +122,6 @@ class FileList
*/
public $counter = 0;

/**
* Counting the elements no matter what
*
* @var int
*/
public $eCounter = 0;

/**
* @var TranslationConfigurationProvider
*/
Expand Down Expand Up @@ -186,7 +179,7 @@ class FileList
*/
protected $uriBuilder;

protected string $searchTerm = '';
protected ?FileSearchDemand $searchDemand = null;

public function __construct()
{
Expand Down Expand Up @@ -256,7 +249,7 @@ public function linkClipboardHeaderIcon($string, $cmd, $warning = '')
$attributes['data-filelist-clipboard-cmd'] = $cmd;
}

return '<a href="#" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $string . '</a>';
return '<button type="button" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $string . '</button>';
}

/**
Expand All @@ -268,10 +261,12 @@ public function linkClipboardHeaderIcon($string, $cmd, $warning = '')
public function getTable(?FileSearchDemand $searchDemand = null): string
{
if ($searchDemand !== null) {
// Store given search demand
$this->searchDemand = $searchDemand;
// Search currently only works for files
$folders = [];
// Find files by the given search demand
$files = iterator_to_array($this->folderObject->searchFiles($searchDemand));
$files = iterator_to_array($this->folderObject->searchFiles($this->searchDemand));
// @todo Currently files, which got deleted in the file system, are still found.
// Therefore we have to ask their parent folder if it still contains the file.
$files = array_filter($files, static function (FileInterface $file): bool {
Expand All @@ -297,8 +292,6 @@ public function getTable(?FileSearchDemand $searchDemand = null): string

// Add special "Path" field for the search result
array_unshift($this->fieldArray, '_PATH_');
// Add search term so it can be added to return urls
$this->searchTerm = $searchDemand->getSearchTerm() ?? '';
} else {
// @todo use folder methods directly when they support filters
$storage = $this->folderObject->getStorage();
Expand Down Expand Up @@ -352,17 +345,16 @@ public function getTable(?FileSearchDemand $searchDemand = null): string

$iOut = '';
// Directories are added
$this->eCounter = $this->firstElementNumber;
$iOut .= $this->fwd_rwd_nav();
$iOut .= $this->fwd_rwd_nav($this->firstElementNumber);

$iOut .= $this->formatDirList($folders);
// Files are added
$iOut .= $this->formatFileList($files);

$this->eCounter = $this->firstElementNumber + $this->iLimit < $this->totalItems
$amountOfItemsShownOnCurrentPage = $this->firstElementNumber + $this->iLimit < $this->totalItems
? $this->firstElementNumber + $this->iLimit
: -1;
$iOut .= $this->fwd_rwd_nav();
$iOut .= $this->fwd_rwd_nav($amountOfItemsShownOnCurrentPage);

// Header line is drawn
$theData = [];
Expand All @@ -375,7 +367,7 @@ public function getTable(?FileSearchDemand $searchDemand = null): string
$theData[$v] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_'));
} else {
// Normal row
$theData[$v] = $this->linkWrapSort($this->folderObject->getCombinedIdentifier(), $v);
$theData[$v] = $this->linkWrapSort($v);
}
}

Expand Down Expand Up @@ -433,7 +425,10 @@ protected function renderClipboardHeaderRow(bool $hasContent): string
$cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_deleteMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span>', 'delete', $this->getLanguageService()->getLL('clip_deleteMarkedWarning'));
$cells[] = '<a class="btn btn-default t3js-toggle-all-checkboxes" data-checkboxes-names="' . htmlspecialchars(implode(',', $this->CBnames)) . '" rel="" href="#" title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_markRecords')) . '">' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>';
}
return implode('', $cells);
if (!empty($cells)) {
return '<div class="btn-group">' . implode('', $cells) . '</div>';
}
return '';
}

/**
Expand Down Expand Up @@ -508,57 +503,35 @@ public function addElement($icon, $data, $colType = 'td')
*
* @return string the table-row code for the element
*/
public function fwd_rwd_nav()
public function fwd_rwd_nav(int $currentItemCount): string
{
$code = '';
if ($this->eCounter >= $this->firstElementNumber && $this->eCounter < $this->firstElementNumber + $this->iLimit) {
if ($this->firstElementNumber && $this->eCounter == $this->firstElementNumber) {
if ($currentItemCount >= $this->firstElementNumber && $currentItemCount < $this->firstElementNumber + $this->iLimit) {
if ($this->firstElementNumber && $currentItemCount == $this->firstElementNumber) {
// Reverse
$theData = [];
$theData['file'] = $this->fwd_rwd_HTML('fwd', $this->eCounter);
$href = $this->listURL(['pointer' => ($currentItemCount - $this->iLimit)]);
$theData['file'] = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
'actions-move-up',
Icon::SIZE_SMALL
)->render() . ' <i>[' . (max(0, $currentItemCount - $this->iLimit) + 1) . ' - ' . $currentItemCount . ']</i></a>';
$code = $this->addElement('', $theData);
}
return $code;
}
if ($this->eCounter == $this->firstElementNumber + $this->iLimit) {
if ($currentItemCount === $this->firstElementNumber + $this->iLimit) {
// Forward
$theData = [];
$theData['file'] = $this->fwd_rwd_HTML('rwd', $this->eCounter);
$href = $this->listURL(['pointer' => $currentItemCount]);
$theData['file'] = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
'actions-move-down',
Icon::SIZE_SMALL
)->render() . ' <i>[' . ($currentItemCount + 1) . ' - ' . $this->totalItems . ']</i></a>';
$code = $this->addElement('', $theData);
}
return $code;
}

/**
* Creates the button with link to either forward or reverse
*
* @param string $type Type: "fwd" or "rwd
* @param int $pointer Pointer
* @return string
* @internal
*/
public function fwd_rwd_HTML($type, $pointer)
{
$content = '';
switch ($type) {
case 'fwd':
$href = $this->listURL() . '&pointer=' . ($pointer - $this->iLimit);
$content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
'actions-move-up',
Icon::SIZE_SMALL
)->render() . ' <i>[' . (max(0, $pointer - $this->iLimit) + 1) . ' - ' . $pointer . ']</i></a>';
break;
case 'rwd':
$href = $this->listURL() . '&pointer=' . $pointer;
$content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
'actions-move-down',
Icon::SIZE_SMALL
)->render() . ' <i>[' . ($pointer + 1) . ' - ' . $this->totalItems . ']</i></a>';
break;
}
return $content;
}

/**
* Gets the number of files and total size of a folder
*
Expand Down Expand Up @@ -666,7 +639,7 @@ public function formatDirList(array $folders)
*/
public function linkWrapDir($title, Folder $folderObject)
{
$href = (string)$this->uriBuilder->buildUriFromRoute('file_FilelistList', ['id' => $folderObject->getCombinedIdentifier()]);
$href = $this->listURL(['id' => $folderObject->getCombinedIdentifier(), 'searchTerm' => '', 'pointer' => 0]);
$triggerTreeUpdateAttribute = sprintf(
' data-tree-update-request="%s"',
htmlspecialchars($folderObject->getCombinedIdentifier())
Expand Down Expand Up @@ -710,17 +683,18 @@ public function linkWrapFile($code, File $fileObject)

/**
* Returns list URL; This is the URL of the current script with id and imagemode parameters, that's all.
* The URL however is not relative, otherwise GeneralUtility::sanitizeLocalUrl() would say that
* the URL would be invalid.
*
* @return string URL
*/
public function listURL(): string
public function listURL(array $params = []): string
{
return GeneralUtility::linkThisScript(array_filter([
'target' => rawurlencode($this->folderObject->getCombinedIdentifier()),
'searchTerm' => rawurlencode($this->searchTerm)
]));
$params = array_replace_recursive([
'pointer' => $this->firstElementNumber,
'id' => $this->folderObject->getCombinedIdentifier(),
'searchTerm' => $this->searchDemand ? $this->searchDemand->getSearchTerm() : ''
], $params);
$params = array_filter($params);
return (string)$this->uriBuilder->buildUriFromRoute('file_FilelistList', $params);
}

protected function getAvailableSystemLanguages(): array
Expand Down Expand Up @@ -903,14 +877,13 @@ protected function getTranslationsForMetaData($metaDataRecord)
/**
* Wraps the directory-titles ($code) in a link to filelist/Modules/Filelist/index.php (id=$path) and sorting commands...
*
* @param string $folderIdentifier ID (path)
* @param string $col Sorting column
* @return string HTML
*/
public function linkWrapSort($folderIdentifier, $col)
public function linkWrapSort($col)
{
$code = htmlspecialchars($this->getLanguageService()->getLL('c_' . $col));
$params = ['id' => $folderIdentifier, 'SET' => ['sort' => $col]];
$params = ['SET' => ['sort' => $col], 'pointer' => 0];

if ($this->sort === $col) {
// Check reverse sorting
Expand All @@ -920,7 +893,7 @@ public function linkWrapSort($folderIdentifier, $col)
$params['SET']['reverse'] = 0;
$sortArrow = '';
}
$href = (string)$this->uriBuilder->buildUriFromRoute('file_FilelistList', $params);
$href = $this->listURL($params);
return '<a href="' . htmlspecialchars($href) . '">' . $code . ' ' . $sortArrow . '</a>';
}

Expand Down

0 comments on commit 1eda452

Please sign in to comment.