Commit 1eda4529 authored by Benni Mack's avatar Benni Mack
Browse files

[BUGFIX] Allow proper back-linking in File List

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's avatarOliver Bartsch <bo@cedev.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 9ea6ad02
......@@ -44,6 +44,10 @@
}
}
label.btn-checkbox {
margin-bottom: 0;
}
.btn-clear {
&:focus {
outline: 1px dotted #000;
......
......@@ -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"]'
}
/**
......@@ -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 {
......@@ -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 => {
......@@ -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();
}
}
......
......@@ -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;
......@@ -108,6 +109,7 @@ class FileListController implements LoggerAwareInterface
$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
);
......@@ -364,7 +366,7 @@ class FileListController implements LoggerAwareInterface
// 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)
......@@ -427,7 +429,10 @@ class FileListController implements LoggerAwareInterface
$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', [
......
......@@ -122,13 +122,6 @@ class FileList
*/
public $counter = 0;
/**
* Counting the elements no matter what
*
* @var int
*/
public $eCounter = 0;
/**
* @var TranslationConfigurationProvider
*/
......@@ -186,7 +179,7 @@ class FileList
*/
protected $uriBuilder;
protected string $searchTerm = '';
protected ?FileSearchDemand $searchDemand = null;
public function __construct()
{
......@@ -256,7 +249,7 @@ class FileList
$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>';
}
/**
......@@ -268,10 +261,12 @@ class FileList
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 {
......@@ -297,8 +292,6 @@ class FileList
// 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();
......@@ -352,17 +345,16 @@ class FileList
$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 = [];
......@@ -375,7 +367,7 @@ class FileList
$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);
}
}
......@@ -433,7 +425,10 @@ class FileList
$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 '';
}
/**
......@@ -508,57 +503,35 @@ class FileList
*
* @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
*
......@@ -666,7 +639,7 @@ class FileList
*/
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())
......@@ -710,17 +683,18 @@ class FileList
/**
* 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
......@@ -903,14 +877,13 @@ class FileList
/**
* 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
......@@ -920,7 +893,7 @@ class FileList
$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>';
}
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Core/DocumentService","TYPO3/CMS/Backend/InfoWindow","TYPO3/CMS/Backend/BroadcastMessage","TYPO3/CMS/Backend/BroadcastService","TYPO3/CMS/Backend/Tooltip","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,i,o,a,s,r,l){"use strict";var n;!function(e){e.fileListFormSelector='form[name="fileListForm"]',e.commandSelector='input[name="cmd"]',e.searchFieldSelector='input[name="searchTerm"]'}(n||(n={}));class c{constructor(){this.fileListForm=document.querySelector(n.fileListFormSelector),this.command=this.fileListForm.querySelector(n.commandSelector),this.searchField=this.fileListForm.querySelector(n.searchFieldSelector),this.activeSearch=""!==this.searchField.value,c.processTriggers(),i.ready().then(()=>{r.initialize(".table-fit a[title]"),c.registerTreeUpdateEvents(),new l("click",(e,t)=>{e.preventDefault(),c.openInfoPopup(t.dataset.filelistShowItemType,t.dataset.filelistShowItemIdentifier)}).delegateTo(document,"[data-filelist-show-item-identifier][data-filelist-show-item-type]"),new l("click",(e,t)=>{e.preventDefault(),c.openInfoPopup("_FILE",t.dataset.identifier)}).delegateTo(document,"a.filelist-file-info"),new l("click",(e,t)=>{e.preventDefault(),c.openInfoPopup("_FILE",t.dataset.identifier)}).delegateTo(document,"a.filelist-file-references"),new l("click",(e,t)=>{e.preventDefault();const i=t.getAttribute("href");let o=i?encodeURIComponent(i):encodeURIComponent(top.list_frame.document.location.pathname+top.list_frame.document.location.search);top.list_frame.location.href=i+"&redirect="+o}).delegateTo(document,"a.filelist-file-copy");const e=document.querySelector('[data-event-name="filelist:clipboard:cmd"]');null!==e&&new l("filelist:clipboard:cmd",(e,t)=>{e.detail.result&&this.submitClipboardFormWithCommand(e.detail.payload)}).bindTo(e),new l("click",(e,t)=>{const i=t.dataset.filelistClipboardCmd;this.submitClipboardFormWithCommand(i)}).delegateTo(document,'[data-filelist-clipboard-cmd]:not([data-filelist-clipboard-cmd=""])')}),new l("search",()=>{""===this.searchField.value&&this.activeSearch&&this.fileListForm.submit()}).bindTo(this.searchField)}static openInfoPopup(e,t){o.showItem(e,t)}static processTriggers(){const e=document.querySelector(".filelist-main");if(null!==e&&(c.emitTreeUpdateRequest(e.dataset.filelistCurrentFolderHash),top.fsMod)){const t=encodeURIComponent(e.dataset.filelistCurrentIdentifier);"object"!=typeof top.fsMod.recentIds?top.fsMod.recentIds={file:t}:top.fsMod.recentIds.file=t}}static registerTreeUpdateEvents(){new l("click",(function(){c.emitTreeUpdateRequest(this.dataset.treeUpdateRequest)})).delegateTo(document.body,"[data-tree-update-request]")}static emitTreeUpdateRequest(e){const t=new a.BroadcastMessage("filelist","treeUpdateRequested",{type:"folder",identifier:e});s.post(t)}submitClipboardFormWithCommand(e){this.command.value=e,this.fileListForm.submit()}}return new c}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Core/DocumentService","TYPO3/CMS/Backend/InfoWindow","TYPO3/CMS/Backend/BroadcastMessage","TYPO3/CMS/Backend/BroadcastService","TYPO3/CMS/Backend/Tooltip","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,i,o,r,s,a,l){"use strict";var n;!function(e){e.fileListFormSelector='form[name="fileListForm"]',e.commandSelector='input[name="cmd"]',e.searchFieldSelector='input[name="searchTerm"]',e.pointerFieldSelector='input[name="pointer"]'}(n||(n={}));class c{constructor(){this.fileListForm=document.querySelector(n.fileListFormSelector),this.command=this.fileListForm.querySelector(n.commandSelector),this.searchField=this.fileListForm.querySelector(n.searchFieldSelector),this.pointerField=this.fileListForm.querySelector(n.pointerFieldSelector),this.activeSearch=""!==this.searchField.value,c.processTriggers(),i.ready().then(()=>{a.initialize(".table-fit a[title]"),c.registerTreeUpdateEvents(),new l("click",(e,t)=>{e.preventDefault(),c.openInfoPopup(t.dataset.filelistShowItemType,t.dataset.filelistShowItemIdentifier)}).delegateTo(document,"[data-filelist-show-item-identifier][data-filelist-show-item-type]"),new l("click",(e,t)=>{e.preventDefault(),c.openInfoPopup("_FILE",t.dataset.identifier)}).delegateTo(document,"a.filelist-file-info"),new l("click",(e,t)=>{e.preventDefault(),c.openInfoPopup("_FILE",t.dataset.identifier)}).delegateTo(document,"a.filelist-file-references"),new l("click",(e,t)=>{e.preventDefault();const i=t.getAttribute("href");let o=i?encodeURIComponent(i):encodeURIComponent(top.list_frame.document.location.pathname+top.list_frame.document.location.search);top.list_frame.location.href=i+"&redirect="+o}).delegateTo(document,"a.filelist-file-copy");const e=document.querySelector('[data-event-name="filelist:clipboard:cmd"]');null!==e&&new l("filelist:clipboard:cmd",(e,t)=>{e.detail.result&&this.submitClipboardFormWithCommand(e.detail.payload)}).bindTo(e),new l("click",(e,t)=>{const i=t.dataset.filelistClipboardCmd;this.submitClipboardFormWithCommand(i)}).delegateTo(document,'[data-filelist-clipboard-cmd]:not([data-filelist-clipboard-cmd=""])')}),new l("search",()=>{""===this.searchField.value&&this.activeSearch&&this.fileListForm.submit()}).bindTo(this.searchField)}static openInfoPopup(e,t){o.showItem(e,t)}static processTriggers(){const e=document.querySelector(".filelist-main");if(null!==e&&(c.emitTreeUpdateRequest(e.dataset.filelistCurrentFolderHash),top.fsMod)){const t=encodeURIComponent(e.dataset.filelistCurrentIdentifier);"object"!=typeof top.fsMod.recentIds?top.fsMod.recentIds={file:t}:top.fsMod.recentIds.file=t}}static registerTreeUpdateEvents(){new l("click",(function(){c.emitTreeUpdateRequest(this.dataset.treeUpdateRequest)})).delegateTo(document.body,"[data-tree-update-request]")}static emitTreeUpdateRequest(e){const t=new r.BroadcastMessage("filelist","treeUpdateRequested",{type:"folder",identifier:e});s.post(t)}static parseQueryParameters(e){let t={};if(e&&Object.prototype.hasOwnProperty.call(e,"search")){let i=e.search.substr(1).split("&");for(let e=0;e<i.length;e++){const o=i[e].split("=");t[decodeURIComponent(o[0])]=decodeURIComponent(o[1])}}return t}submitClipboardFormWithCommand(e){if(this.command.value=e,"setCB"===e){const e=c.parseQueryParameters(document.location).pointer;e&&(this.pointerField.value=e)}this.fileListForm.submit()}}return new c}));
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment