Commit 7ef32708 authored by Łukasz Uznański's avatar Łukasz Uznański Committed by Jigal van Hemert
Browse files

[FEATURE] Add button to select all records

Add and handle button to select all records from all pages in recycler.
Right now, there is pagination, which means that you can select 50 records max.

Resolves: #81310
Releases: master
Change-Id: Icfc0c93e5cff5cd9573a6a39b615ce0c6e1d273c
Reviewed-on: https://review.typo3.org/54849


Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: default avatarTobi Kretschmann <tobi@tobishome.de>
Tested-by: default avatarTobi Kretschmann <tobi@tobishome.de>
Reviewed-by: default avatarJoerg Boesche <typo3@joergboesche.de>
Tested-by: default avatarJoerg Boesche <typo3@joergboesche.de>
Reviewed-by: default avatarSteffen Frese <steffenf14@gmail.com>
Reviewed-by: Jigal van Hemert's avatarJigal van Hemert <jigal.van.hemert@typo3.org>
Tested-by: Jigal van Hemert's avatarJigal van Hemert <jigal.van.hemert@typo3.org>
parent 861e6c02
.. include:: ../../Includes.txt
==================================================================
Feature: #54849 - Add button to select all records in EXT:recycler
==================================================================
See :issue:`54849`
Description
===========
Add button to select all records from all pages in EXT:recycler.
Impact
======
All TYPO3 installations where EXT:recycler is enabled.
.. index:: Backend, ext:recycler
\ No newline at end of file
......@@ -50,12 +50,10 @@ class DeletedRecordsController
* Transforms the rows for the deleted records
*
* @param array $deletedRowsArray Array with table as key and array with all deleted rows
* @param int $totalDeleted Number of deleted records in total
* @return array JSON array
*/
public function transform($deletedRowsArray, $totalDeleted)
public function transform($deletedRowsArray)
{
$total = 0;
$jsonArray = [
'rows' => []
];
......@@ -65,7 +63,6 @@ class DeletedRecordsController
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
foreach ($deletedRowsArray as $table => $rows) {
$total += count($deletedRowsArray[$table]);
foreach ($rows as $row) {
$pageTitle = $this->getPageTitle((int)$row['pid']);
$backendUserName = $this->getBackendUser((int)$row[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']]);
......@@ -92,7 +89,29 @@ class DeletedRecordsController
}
}
}
$jsonArray['total'] = $totalDeleted;
return $jsonArray;
}
/**
* Transforms the rows for the deleted records
*
* @param array $deletedRowsArray Array with table as key and array with all deleted rows
* @return array JSON array
*/
public function transformSmallAddTotal(array $deletedRowsArray): array
{
$jsonArray = [];
$total = 0;
if (is_array($deletedRowsArray)) {
foreach ($deletedRowsArray as $table => $rows) {
foreach ($rows as $row) {
$key = $table . ':' . $row['uid'];
$jsonArray['rows'][$key] = 1;
$total++;
}
}
}
$jsonArray['total'] = $total;
return $jsonArray;
}
......
......@@ -54,7 +54,7 @@ class RecyclerAjaxController
$this->conf['filterTxt'] = GeneralUtility::_GP('filterTxt') ? GeneralUtility::_GP('filterTxt') : '';
$this->conf['startUid'] = GeneralUtility::_GP('startUid') ? (int)GeneralUtility::_GP('startUid') : 0;
$this->conf['depth'] = GeneralUtility::_GP('depth') ? (int)GeneralUtility::_GP('depth') : 0;
$this->conf['records'] = GeneralUtility::_GP('records') ? GeneralUtility::_GP('records') : null;
$this->conf['records'] = json_decode(GeneralUtility::_GP('records') ? GeneralUtility::_GP('records') : '[]', true);
$this->conf['recursive'] = GeneralUtility::_GP('recursive') ? (bool)GeneralUtility::_GP('recursive') : false;
}
......@@ -94,11 +94,13 @@ class RecyclerAjaxController
$deletedRowsArray = $model->getDeletedRows();
$model = GeneralUtility::makeInstance(DeletedRecords::class);
$totalDeleted = $model->getTotalCount($this->conf['startUid'], $this->conf['table'], $this->conf['depth'], $this->conf['filterTxt']);
$model->loadData($this->conf['startUid'], $this->conf['table'], $this->conf['depth'], null, $this->conf['filterTxt']);
$deletedRowsArrayAll = $model->getDeletedRows();
/* @var $controller DeletedRecordsController */
$controller = GeneralUtility::makeInstance(DeletedRecordsController::class);
$recordsArray = $controller->transform($deletedRowsArray, $totalDeleted);
$recordsArray = $controller->transform($deletedRowsArray);
$recordsArrayAll = $controller->transformSmallAddTotal($deletedRowsArrayAll);
$modTS = $this->getBackendUser()->getTSConfig('mod.recycler');
$allowDelete = $this->getBackendUser()->isAdmin() ? true : (bool)$modTS['properties']['allowDelete'];
......@@ -109,7 +111,8 @@ class RecyclerAjaxController
$view->assign('total', $recordsArray['total']);
$content = [
'rows' => $view->render(),
'totalItems' => $recordsArray['total']
'totalItems' => $recordsArrayAll['total'],
'allTheRows' => $recordsArrayAll['rows']
];
break;
case 'undoRecords':
......
......@@ -55,7 +55,7 @@ class RecyclerModuleController extends ActionController
/**
* @var int
*/
protected $recordsPageLimit = 50;
protected $recordsPageLimit = 25;
/**
* @var int
......
......@@ -18,6 +18,18 @@
<trans-unit id="button.delete">
<source>Delete</source>
</trans-unit>
<trans-unit id="button.selectall">
<source>Select all records from all pages</source>
</trans-unit>
<trans-unit id="button.deselectall">
<source>Deselect all</source>
</trans-unit>
<trans-unit id="button.selectallamount">
<source>Select all records ({0}) from all pages</source>
</trans-unit>
<trans-unit id="button.selectallamountrest">
<source>Select the rest of the records ({0}) from all pages</source>
</trans-unit>
<trans-unit id="button.deleteselected">
<source>Delete {0} records</source>
</trans-unit>
......
......@@ -44,21 +44,44 @@
</tbody>
</table>
</div>
<div class="progress progress-bar-notice alert-loading" style="display: none">
<div class="t3js-progressbar progress-bar progress-bar-striped active m-3"
role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"
style="width: 100%; height:25px; ">
<span class="sr-only">Loading...</span>
</div>
</div>
<div>
<button class="btn btn-default disabled" data-action="massundo">
<core:icon identifier="actions-edit-undo" />
<span class="text">
<f:translate key="button.undo" />
</span>
</button>
<f:if condition="{allowDelete}">
<button class="btn btn-default disabled" data-action="massdelete">
<core:icon identifier="actions-edit-delete" />
<div>
<button class="btn btn-default disabled" data-action="selectall">
<core:icon identifier="actions-document-select" />
<span class="text">
<f:translate key="LLL:EXT:recycler/Resources/Private/Language/locallang.xlf:button.selectall"/>
</span>
</button>
<button class="btn btn-default disabled" data-action="deselectall">
<span class="text">
<f:translate key="button.delete" />
<f:translate key="LLL:EXT:recycler/Resources/Private/Language/locallang.xlf:button.deselectall"/>
</span>
</button>
</f:if>
</div>
<div>
<button class="btn btn-default disabled" data-action="massundo">
<core:icon identifier="actions-edit-undo" />
<span class="text">
<f:translate key="button.undo" />
</span>
</button>
<f:if condition="{allowDelete}">
<button class="btn btn-default disabled" data-action="massdelete">
<core:icon identifier="actions-edit-delete" />
<span class="text">
<f:translate key="button.delete" />
</span>
</button>
</f:if>
</div>
</div>
<nav>
</nav>
......
......@@ -41,7 +41,10 @@ define(['jquery',
reloadAction: 'a[data-action=reload]',
massUndo: 'button[data-action=massundo]',
massDelete: 'button[data-action=massdelete]',
toggleAll: '.t3js-toggle-all'
selectAll: 'button[data-action=selectall]',
deselectAll: 'button[data-action=deselectall]',
toggleAll: '.t3js-toggle-all',
progressBar: '#recycler-index .progress.progress-bar-notice.alert-loading'
},
elements: {}, // filled in getElements()
paging: {
......@@ -70,7 +73,10 @@ define(['jquery',
$reloadAction: $(Recycler.identifiers.reloadAction),
$massUndo: $(Recycler.identifiers.massUndo),
$massDelete: $(Recycler.identifiers.massDelete),
$toggleAll: $(Recycler.identifiers.toggleAll)
$selectAll: $(Recycler.identifiers.selectAll),
$deselectAll: $(Recycler.identifiers.deselectAll),
$toggleAll: $(Recycler.identifiers.toggleAll),
$progressBar: $(Recycler.identifiers.progressBar)
};
};
......@@ -108,6 +114,7 @@ define(['jquery',
// changing "depth"
Recycler.elements.$depthSelector.on('change', function() {
$.when(Recycler.loadAvailableTables()).done(function() {
Recycler.clearMarked();
Recycler.loadDeletedElements();
});
});
......@@ -115,6 +122,7 @@ define(['jquery',
// changing "table"
Recycler.elements.$tableSelector.on('change', function() {
Recycler.paging.currentPage = 1;
Recycler.clearMarked();
Recycler.loadDeletedElements();
});
......@@ -159,6 +167,7 @@ define(['jquery',
if (reload) {
Recycler.loadDeletedElements();
Recycler.loadMarked();
}
});
......@@ -189,14 +198,31 @@ define(['jquery',
});
// checkboxes in the table
Recycler.elements.$toggleAll.on('click', function() {
Recycler.allToggled = !Recycler.allToggled;
$('input[type="checkbox"]').prop('checked', Recycler.allToggled).trigger('change');
});
Recycler.elements.$recyclerTable.on('change', 'tr input[type=checkbox]', Recycler.handleCheckboxSelects);
Recycler.elements.$massUndo.on('click', Recycler.undoRecord);
Recycler.elements.$massDelete.on('click', Recycler.deleteRecord);
Recycler.elements.$toggleAll.on('click', Recycler.toggleAll);
Recycler.elements.$massUndo.on('click', function() {
if (!$(this).hasClass('disabled')) {
Recycler.undoRecord();
}
});
Recycler.elements.$massDelete.on('click', function() {
if (!$(this).hasClass('disabled')) {
Recycler.deleteRecord();
}
});
Recycler.elements.$selectAll.on('click', function() {
if (!$(this).hasClass('disabled')) {
Recycler.selectAll();
}
});
Recycler.elements.$deselectAll.on('click', function() {
if (!$(this).hasClass('disabled')) {
Recycler.deselectAll();
}
});
};
/**
......@@ -226,46 +252,40 @@ define(['jquery',
table = $tr.data('table'),
uid = $tr.data('uid'),
record = table + ':' + uid;
if ($checkbox.prop('checked')) {
Recycler.markedRecordsForMassAction.push(record);
$tr.addClass('warning');
} else {
var index = Recycler.markedRecordsForMassAction.indexOf(record);
if (index > -1) {
Recycler.markedRecordsForMassAction.splice(index, 1);
}
$tr.removeClass('warning');
}
if (Recycler.markedRecordsForMassAction.length > 0) {
if (Recycler.elements.$massUndo.hasClass('disabled')) {
Recycler.elements.$massUndo.removeClass('disabled');
}
if (Recycler.elements.$massDelete.hasClass('disabled')) {
Recycler.elements.$massDelete.removeClass('disabled');
if (!Recycler.markedRecordsForMassAction[record]) {
Recycler.addRecord(record);
$tr.addClass('warning');
}
var btnTextUndo = Recycler.createMessage(TYPO3.lang['button.undoselected'], [Recycler.markedRecordsForMassAction.length]),
btnTextDelete = Recycler.createMessage(TYPO3.lang['button.deleteselected'], [Recycler.markedRecordsForMassAction.length]);
Recycler.elements.$massUndo.find('span.text').text(btnTextUndo);
Recycler.elements.$massDelete.find('span.text').text(btnTextDelete);
} else {
Recycler.resetMassActionButtons();
if (!!Recycler.markedRecordsForMassAction[record]) {
Recycler.subtractRecord(record);
$tr.removeClass('warning');
}
}
Recycler.selectAllRefresh();
};
/**
* Resets the mass action state
*/
Recycler.resetMassActionButtons = function() {
Recycler.markedRecordsForMassAction = [];
if (!!Recycler.markedRecordsForMassAction) {
Recycler.persistMarked(Recycler.markedRecordsForMassAction);
} else {
Recycler.markedRecordsForMassAction = {};
}
Recycler.elements.$massUndo.addClass('disabled');
Recycler.elements.$massUndo.find('span.text').text(TYPO3.lang['button.undo']);
Recycler.elements.$massDelete.addClass('disabled');
Recycler.elements.$massDelete.find('span.text').text(TYPO3.lang['button.delete']);
Recycler.elements.$selectAll.addClass('disabled');
Recycler.elements.$selectAll.find('span.text').text(TYPO3.lang['button.selectall']);
Recycler.elements.$deselectAll.addClass('disabled');
Recycler.elements.$deselectAll.find('span.text').text(TYPO3.lang['button.deselectall']);
};
/**
......@@ -286,6 +306,7 @@ define(['jquery',
NProgress.start();
Recycler.elements.$tableSelector.val('');
Recycler.paging.currentPage = 1;
Recycler.markedRecordsCounter = 0;
},
success: function(data) {
var tables = [];
......@@ -336,13 +357,24 @@ define(['jquery',
beforeSend: function() {
NProgress.start();
Recycler.resetMassActionButtons();
Recycler.selectAllDataShort = [];
Recycler.currentDataCount = 0;
/** if there are any checkboxes and corresponding buttons, hide them while new content arrives */
Recycler.showLoading();
},
success: function(data) {
var totalItems = data.totalItems;
Recycler.elements.$tableBody.html(data.rows);
Recycler.buildPaginator(data.totalItems);
Recycler.buildPaginator(totalItems);
Recycler.currentDataCount = totalItems;
Recycler.selectAllDataShort = data.allTheRows;
},
complete: function() {
NProgress.done();
Recycler.selectAllRefresh();
}
});
};
......@@ -354,13 +386,12 @@ define(['jquery',
if (TYPO3.settings.Recycler.deleteDisable) {
return;
}
var $tr = $(this).parents('tr'),
isMassDelete = $tr.parent().prop('tagName') !== 'TBODY'; // deleteRecord() was invoked by the mass delete button
var records, message;
if (isMassDelete) {
records = Recycler.markedRecordsForMassAction;
records = Recycler.returnProperMarkedArray();
message = TYPO3.lang['modal.massdelete.text'];
} else {
var uid = $tr.data('uid'),
......@@ -382,7 +413,11 @@ define(['jquery',
text: TYPO3.lang['button.delete'],
btnClass: 'btn-danger',
trigger: function() {
Recycler.callAjaxAction('delete', typeof records === 'object' ? records : [records], isMassDelete);
Recycler.callAjaxAction(
'delete',
typeof records === 'object' ? records : [records],
isMassDelete
)
}
}
]);
......@@ -397,7 +432,7 @@ define(['jquery',
var records, messageText, recoverPages;
if (isMassUndo) {
records = Recycler.markedRecordsForMassAction;
records = Recycler.returnProperMarkedArray();
messageText = TYPO3.lang['modal.massundo.text'];
recoverPages = true;
} else {
......@@ -427,7 +462,7 @@ define(['jquery',
)
);
} else {
$message = messageText;
$message = $('<div />').text(messageText);
}
Modal.confirm(TYPO3.lang['modal.undo.header'], $message, Severity.ok, [
......@@ -441,7 +476,13 @@ define(['jquery',
text: TYPO3.lang['button.undo'],
btnClass: 'btn-success',
trigger: function() {
Recycler.callAjaxAction('undo', typeof records === 'object' ? records : [records], isMassUndo, $message.find('#undo-recursive').prop('checked') ? 1 : 0);
Recycler.callAjaxAction(
'undo',
// typeof records === 'object' ? records : [records],
records,
isMassUndo,
$message.find('#undo-recursive').prop('checked') ? 1 : 0
);
}
}
]);
......@@ -456,10 +497,12 @@ define(['jquery',
*/
Recycler.callAjaxAction = function(action, records, isMassAction, recursive) {
var data = {
records: records,
records: JSON.stringify(records),
action: ''
},
reloadPageTree = false;
reloadPageTree = false,
oldCount = Recycler.markedRecordsCounter,
error = 0;
if (action === 'undo') {
data.action = 'undoRecords';
data.recursive = recursive ? 1 : 0;
......@@ -470,18 +513,23 @@ define(['jquery',
return;
}
$.ajax({
url: TYPO3.settings.ajaxUrls['recycler'],
dataType: 'json',
data: data,
method: 'POST',
beforeSend: function() {
NProgress.start();
/** if there are any checkboxes and corresponding buttons, hide them while new content arrives */
Recycler.showLoading();
},
success: function(data) {
if (data.success) {
Notification.success('', data.message);
} else {
Notification.error('', data.message);
error = 1;
}
// reload recycler data
......@@ -489,16 +537,22 @@ define(['jquery',
$.when(Recycler.loadAvailableTables()).done(function() {
Recycler.loadDeletedElements();
if (isMassAction) {
Recycler.resetMassActionButtons();
if (isMassAction && !error) {
Recycler.clearMarked();
} else {
if (!error) {
if (!!Recycler.markedRecordsForMassAction[records]) {
Recycler.subtractRecord(records);
oldCount--;
}
Recycler.markedRecordsCounter = oldCount;
}
}
if (reloadPageTree) {
Recycler.refreshPageTree();
}
// Reset toggle state
Recycler.allToggled = false;
});
},
complete: function() {
......@@ -592,6 +646,235 @@ define(['jquery',
Recycler.elements.$paginator.html($ul);
};
/**
* Select all records
*/
Recycler.selectAll = function() {
if (Recycler.currentDataCount > 0) {
Recycler.elements.$selectAll.addClass('disabled');
Recycler.markedRecordsForMassAction = {};
Recycler.markedRecordsCounter = Recycler.currentDataCount;
Recycler.markedRecordsForMassAction = $.extend(true, {}, Recycler.selectAllDataShort);
Recycler.elements.$selectAll.removeClass('disabled');
Recycler.selectAllRefresh();
}
};
/**
* Deselect all records and return everything to clean state
*/
Recycler.deselectAll = function() {
Recycler.elements.$deselectAll.addClass('disabled');
Recycler.clearMarked();
Recycler.resetMassActionButtons();
Recycler.selectAllRefresh();
Recycler.elements.$selectAll.removeClass('disabled');
};
/**
* Adjusts mass action buttons to user's action
*/
Recycler.selectAllRefresh = function() {
var totalItems, btnTextSelectAll = '',
btnDisabledArr = ['$deselectAll', '$massUndo', '$massDelete'];
Recycler.hideLoading();
Recycler.persistMarked(Recycler.markedRecordsForMassAction);
Recycler.refreshCheckboxes();
/** if any checkboxes are checked change mass action buttons state */
if (Recycler.markedRecordsCounter > 0) {
var recordsLength = Recycler.markedRecordsCounter,
btnTextDelete = Recycler.createMessage(TYPO3.lang['button.deleteselected'], [recordsLength]),
btnTextUndo = Recycler.createMessage(TYPO3.lang['button.undoselected'], [recordsLength]);
/** if there are any records unselected show the amount */
if (!!Recycler.currentDataCount && ( (Recycler.currentDataCount-Recycler.markedRecordsCounter) > 0 )) {
if (Recycler.markedRecordsCounter === 0) {
btnTextSelectAll = Recycler.createMessage(TYPO3.lang['button.selectallamount'], [Recycler.currentDataCount]);
} else {
var rest = Recycler.currentDataCount - Recycler.markedRecordsCounter;
btnTextSelectAll = Recycler.createMessage(TYPO3.lang['button.selectallamountrest'], [rest]);
}
} else {
btnTextSelectAll = Recycler.createMessage(TYPO3.lang['button.selectall'])
}
/** if total amount of records from ajax is bigger than amount of currently selected records enable selectall */
if (!!Recycler.currentDataCount && (Recycler.currentDataCount > Recycler.markedRecordsCounter)) {
if (Recycler.elements.$selectAll.hasClass('disabled')) {
Recycler.elements.$selectAll.removeClass('disabled');
}
} else {
Recycler.elements.$selectAll.addClass('disabled');
}
/** enable mass action buttons (without selectall)*/
$.each(btnDisabledArr, (function(index, value) {
if (Recycler.elements[value].hasClass('disabled')) {