diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json index 958ff73852..4eeafeeb67 100644 --- a/public/language/en-GB/flags.json +++ b/public/language/en-GB/flags.json @@ -72,5 +72,9 @@ "modal-submit-success": "Content has been flagged for moderation.", "modal-submit-confirm": "Confirm Submission", "modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", - "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." + "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated" } \ No newline at end of file diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js index e9e44f2a74..2dcf7dc091 100644 --- a/public/src/client/flags/list.js +++ b/public/src/client/flags/list.js @@ -5,11 +5,14 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart) Flags.init = function () { Flags.enableFilterForm(); + Flags.enableCheckboxes(); + Flags.handleBulkActions(); - components.get('flags/list').on('click', '[data-flag-id]', function () { - var flagId = this.getAttribute('data-flag-id'); - ajaxify.go('flags/' + flagId); - }); + components.get('flags/list') + .on('click', '[data-flag-id]', function () { + var flagId = this.getAttribute('data-flag-id'); + ajaxify.go('flags/' + flagId); + }); var graphWrapper = $('#flags-daily-wrapper'); var graphFooter = graphWrapper.siblings('.panel-footer'); @@ -38,6 +41,137 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart) }); }; + Flags.enableCheckboxes = function () { + var flagsList = document.querySelector('[component="flags/list"]'); + var checkboxes = flagsList.querySelectorAll('[data-flag-id] input[type="checkbox"]'); + var bulkEl = document.querySelector('[component="flags/bulk-actions"] button'); + var lastClicked; + + document.querySelector('[data-action="toggle-all"]').addEventListener('click', function () { + var state = this.checked; + + checkboxes.forEach(function (el) { + el.checked = state; + }); + }); + + flagsList.addEventListener('click', function (e) { + var subselector = e.target.closest('input[type="checkbox"]'); + if (subselector) { + // Stop checkbox clicks from going into the flag details + e.stopImmediatePropagation(); + + if (lastClicked && e.shiftKey && lastClicked !== subselector) { + // Select all the checkboxes in between + var state = subselector.checked; + var started = false; + + checkboxes.forEach(function (el) { + if ([subselector, lastClicked].some(function (ref) { + return ref === el; + })) { + started = !started; + } + + if (started) { + el.checked = state; + } + }); + } + + // (De)activate bulk actions button based on checkboxes' state + bulkEl.disabled = !Array.prototype.some.call(checkboxes, function (el) { + return el.checked; + }); + + lastClicked = subselector; + } + + // If you miss the checkbox, don't descend into the flag details, either + if (e.target.querySelector('input[type="checkbox"]')) { + e.stopImmediatePropagation(); + } + }); + }; + + Flags.handleBulkActions = function () { + document.querySelector('[component="flags/bulk-actions"]').addEventListener('click', function (e) { + var subselector = e.target.closest('[data-action]'); + if (subselector) { + var action = subselector.getAttribute('data-action'); + var flagIds = Flags.getSelected(); + var promises = []; + + flagIds.forEach(function (flagId) { + promises.push(new Promise(function (resolve, reject) { + var handler = function (err) { + if (err) { + reject(err); + } + + resolve(arguments[1]); + }; + + switch (action) { + case 'bulk-assign': + socket.emit('flags.update', { + flagId: flagId, + data: [ + { + name: 'assignee', + value: app.user.uid, + }, + ], + }, handler); + break; + + case 'bulk-mark-resolved': + socket.emit('flags.update', { + flagId: flagId, + data: [ + { + name: 'state', + value: 'resolved', + }, + ], + }, handler); + break; + } + })); + }); + + Promise.allSettled(promises).then(function (results) { + var fulfilled = results.filter(function (res) { + return res.status === 'fulfilled'; + }).length; + var errors = results.filter(function (res) { + return res.status === 'rejected'; + }); + if (fulfilled) { + app.alertSuccess('[[flags:bulk-success, ' + fulfilled + ']]'); + } + + console.log(errors); + errors.forEach(function (res) { + app.alertError(res.reason); + }); + }); + } + }); + }; + + Flags.getSelected = function () { + var checkboxes = document.querySelectorAll('[component="flags/list"] [data-flag-id] input[type="checkbox"]'); + var payload = []; + checkboxes.forEach(function (el) { + if (el.checked) { + payload.push(el.closest('[data-flag-id]').getAttribute('data-flag-id')); + } + }); + + return payload; + }; + Flags.handleGraphs = function () { var dailyCanvas = document.getElementById('flags:daily'); var dailyLabels = utils.getDaysArray().map(function (text, idx) {