From 61f02f17d857fab6f669a09adfe973ab28ad0239 Mon Sep 17 00:00:00 2001 From: gasoved Date: Mon, 30 Aug 2021 17:42:58 +0300 Subject: [PATCH] feat: column based view on wide priv. tables (#9699) * feat: column based view on wide priv. tables * fix: add group/user * feat: copy buttons to work on visible privs * feat: show what's being copied in modal * feat: optional title and message for category selector modal --- .../en-GB/admin/manage/privileges.json | 13 +-- public/src/admin/manage/privileges.js | 85 ++++++++++++++++--- .../src/admin/modules/checkboxRowSelector.js | 4 +- public/src/modules/categorySelector.js | 4 +- src/categories/create.js | 14 ++- src/socket.io/admin/categories.js | 12 +-- .../partials/categories/select-category.tpl | 5 +- .../admin/partials/privileges/category.tpl | 42 +++------ 8 files changed, 119 insertions(+), 60 deletions(-) diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json index dcd49779f4..8b38b368cd 100644 --- a/public/language/en-GB/admin/manage/privileges.json +++ b/public/language/en-GB/admin/manage/privileges.json @@ -51,10 +51,13 @@ "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", "alert.discarded": "Privilege changes discarded", - "alert.confirm-copyToAll": "Are you sure you wish to apply this privilege set to all categories?", - "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's privilege set to all categories?", - "alert.confirm-copyToChildren": "Are you sure you wish to apply this privilege set to all descendant (child) categories?", - "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's privilege set to all descendant (child) categories?", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", "alert.no-undo": "This action cannot be undone.", - "alert.admin-warning": "Administrators implicitly get all privileges" + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." } \ No newline at end of file diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index a8040af1ef..cae10f58c0 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -12,6 +12,8 @@ define('admin/manage/privileges', [ var Privileges = {}; var cid; + // number of columns to skip in category privilege tables + const SKIP_PRIV_COLS = 3; Privileges.init = function () { cid = isNaN(parseInt(ajaxify.data.selectedCategory.cid, 10)) ? 'admin' : ajaxify.data.selectedCategory.cid; @@ -33,6 +35,7 @@ define('admin/manage/privileges', [ Privileges.setupPrivilegeTable(); highlightRow(); + $('.privilege-filters button:last-child').click(); }; Privileges.setupPrivilegeTable = function () { @@ -100,39 +103,43 @@ define('admin/manage/privileges', [ } }); - $('.privilege-table-container').on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable); - $('.privilege-table-container').on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable); - $('.privilege-table-container').on('click', '[data-action="copyToChildren"]', function () { + const $privTableCon = $('.privilege-table-container'); + $privTableCon.on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable); + $privTableCon.on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable); + $privTableCon.on('click', '[data-action="copyToChildren"]', function () { throwConfirmModal('copyToChildren', Privileges.copyPrivilegesToChildren.bind(null, cid, '')); }); - $('.privilege-table-container').on('click', '[data-action="copyToChildrenGroup"]', function () { + $privTableCon.on('click', '[data-action="copyToChildrenGroup"]', function () { var groupName = $(this).parents('[data-group-name]').attr('data-group-name'); throwConfirmModal('copyToChildrenGroup', Privileges.copyPrivilegesToChildren.bind(null, cid, groupName)); }); - $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', function () { + $privTableCon.on('click', '[data-action="copyPrivilegesFrom"]', function () { Privileges.copyPrivilegesFromCategory(cid, ''); }); - $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFromGroup"]', function () { + $privTableCon.on('click', '[data-action="copyPrivilegesFromGroup"]', function () { var groupName = $(this).parents('[data-group-name]').attr('data-group-name'); Privileges.copyPrivilegesFromCategory(cid, groupName); }); - $('.privilege-table-container').on('click', '[data-action="copyToAll"]', function () { + $privTableCon.on('click', '[data-action="copyToAll"]', function () { throwConfirmModal('copyToAll', Privileges.copyPrivilegesToAllCategories.bind(null, cid, '')); }); - $('.privilege-table-container').on('click', '[data-action="copyToAllGroup"]', function () { + $privTableCon.on('click', '[data-action="copyToAllGroup"]', function () { var groupName = $(this).parents('[data-group-name]').attr('data-group-name'); throwConfirmModal('copyToAllGroup', Privileges.copyPrivilegesToAllCategories.bind(null, cid, groupName)); }); + $privTableCon.on('click', '.privilege-filters > button', filterPrivileges); + mousetrap.bind('ctrl+s', function (ev) { throwConfirmModal('save', Privileges.commit); ev.preventDefault(); }); function throwConfirmModal(method, onConfirm) { - bootbox.confirm('[[admin/manage/privileges:alert.confirm-' + method + ']]

[[admin/manage/privileges:alert.no-undo]]', function (ok) { + const privilegeSubset = getPrivilegeSubset(); + bootbox.confirm(`[[admin/manage/privileges:alert.confirm-${method}, ${privilegeSubset}]]

[[admin/manage/privileges:alert.no-undo]]`, function (ok) { if (ok) { onConfirm.call(); } @@ -175,9 +182,16 @@ define('admin/manage/privileges', [ ajaxify.data.privileges = { ...ajaxify.data.privileges, ...privileges }; var tpl = parseInt(cid, 10) ? 'admin/partials/privileges/category' : 'admin/partials/privileges/global'; app.parseAndTranslate(tpl, { privileges }).then((html) => { + // Get currently selected filters + const btnIndices = $('.privilege-filters button.btn-warning').map((idx, el) => $(el).index()).get(); $('.privilege-table-container').html(html); Privileges.exposeAssumedPrivileges(); - checkboxRowSelector.updateAll(); + document.querySelectorAll('.privilege-filters').forEach((con, i) => { + // Three buttons, placed in reverse order + const lastIdx = $('.privilege-filters').first().find('button').length - 1; + const idx = btnIndices[i] === undefined ? lastIdx : btnIndices[i]; + con.querySelectorAll('button')[idx].click(); + }); hightlightRowByDataAttr('data-group-name', groupToHighlight); }); @@ -254,7 +268,8 @@ define('admin/manage/privileges', [ }; Privileges.copyPrivilegesToChildren = function (cid, group) { - socket.emit('admin.categories.copyPrivilegesToChildren', { cid: cid, group: group }, function (err) { + const filter = getPrivilegeFilter(); + socket.emit('admin.categories.copyPrivilegesToChildren', { cid, group, filter }, function (err) { if (err) { return app.alertError(err.message); } @@ -263,12 +278,20 @@ define('admin/manage/privileges', [ }; Privileges.copyPrivilegesFromCategory = function (cid, group) { + const privilegeSubset = getPrivilegeSubset(); + const message = '
' + + (group ? `[[admin/manage/privileges:alert.copyPrivilegesFromGroup-warning, ${privilegeSubset}]]` : + `[[admin/manage/privileges:alert.copyPrivilegesFrom-warning, ${privilegeSubset}]]`) + + '

[[admin/manage/privileges:alert.no-undo]]'; categorySelector.modal({ + title: '[[admin/manage/privileges:alert.copyPrivilegesFrom-title]]', + message, localCategories: [], showLinks: true, onSubmit: function (selectedCategory) { socket.emit('admin.categories.copyPrivilegesFrom', { toCid: cid, + filter: getPrivilegeFilter(), fromCid: selectedCategory.cid, group: group, }, function (err) { @@ -282,7 +305,8 @@ define('admin/manage/privileges', [ }; Privileges.copyPrivilegesToAllCategories = function (cid, group) { - socket.emit('admin.categories.copyPrivilegesToAllCategories', { cid: cid, group: group }, function (err) { + const filter = getPrivilegeFilter(); + socket.emit('admin.categories.copyPrivilegesToAllCategories', { cid, group, filter }, function (err) { if (err) { return app.alertError(err.message); } @@ -369,9 +393,11 @@ define('admin/manage/privileges', [ }, }, function (html) { var tbodyEl = document.querySelector('.privilege-table tbody'); + const btnIdx = $('.privilege-filters').first().find('button.btn-warning').index(); tbodyEl.append(html.get(0)); Privileges.exposeAssumedPrivileges(); hightlightRowByDataAttr('data-group-name', group); + document.querySelector('.privilege-filters').querySelectorAll('button')[btnIdx].click(); cb(); }); } @@ -406,11 +432,46 @@ define('admin/manage/privileges', [ }); var tbodyEl = document.querySelectorAll('.privilege-table tbody'); + const btnIdx = $('.privilege-filters').last().find('button.btn-warning').index(); tbodyEl[1].append(html.get(0)); Privileges.exposeAssumedPrivileges(); hightlightRowByDataAttr('data-uid', user.uid); + document.querySelectorAll('.privilege-filters')[1].querySelectorAll('button')[btnIdx].click(); cb(); } + function filterPrivileges(ev) { + const [startIdx, endIdx] = ev.target.getAttribute('data-filter').split(',').map(i => parseInt(i, 10)); + const rows = $(ev.target).closest('table')[0].querySelectorAll('thead tr:last-child, tbody tr '); + rows.forEach((tr) => { + tr.querySelectorAll('td, th').forEach((el, idx) => { + const offset = el.tagName.toUpperCase() === 'TH' ? 1 : 0; + if (idx < (SKIP_PRIV_COLS - offset)) { + return; + } + el.classList.toggle('hidden', !(idx >= (startIdx - offset) && idx <= (endIdx - offset))); + }); + }); + checkboxRowSelector.updateAll(); + $(ev.target).siblings('button').toArray().forEach(btn => btn.classList.remove('btn-warning')); + ev.target.classList.add('btn-warning'); + } + + function getPrivilegeFilter() { + const indices = document.querySelector('.privilege-filters .btn-warning') + .getAttribute('data-filter') + .split(',') + .map(i => parseInt(i, 10)); + indices[0] -= SKIP_PRIV_COLS; + indices[1] = indices[1] - SKIP_PRIV_COLS + 1; + return indices; + } + + function getPrivilegeSubset() { + const currentPrivFilter = document.querySelector('.privilege-filters .btn-warning'); + const filterText = currentPrivFilter ? currentPrivFilter.textContent.toLocaleLowerCase() : ''; + return filterText.indexOf('privileges') > -1 ? filterText : `${filterText} privileges`.trim(); + } + return Privileges; }); diff --git a/public/src/admin/modules/checkboxRowSelector.js b/public/src/admin/modules/checkboxRowSelector.js index 7d41fc6914..ea50b3cc0b 100644 --- a/public/src/admin/modules/checkboxRowSelector.js +++ b/public/src/admin/modules/checkboxRowSelector.js @@ -21,7 +21,7 @@ define('admin/modules/checkboxRowSelector', function () { if (self.toggling) { return; } - const checkboxes = $checkboxEl.closest('tr').find('input:not([disabled])').toArray(); + const checkboxes = $checkboxEl.closest('tr').find('input:not([disabled]):visible').toArray(); const $toggler = $(checkboxes.shift()); const rowState = checkboxes.length && checkboxes.every(el => el.checked); $toggler.prop('checked', rowState); @@ -35,7 +35,7 @@ define('admin/modules/checkboxRowSelector', function () { function toggleAll($checkboxEl) { self.toggling = true; const state = $checkboxEl.prop('checked'); - $checkboxEl.closest('tr').find('input:not(.checkbox-helper)').each((idx, el) => { + $checkboxEl.closest('tr').find('input:not(.checkbox-helper):visible').each((idx, el) => { const $checkbox = $(el); if ($checkbox.prop('checked') === state) { return; diff --git a/public/src/modules/categorySelector.js b/public/src/modules/categorySelector.js index 3117857999..f7de882276 100644 --- a/public/src/modules/categorySelector.js +++ b/public/src/modules/categorySelector.js @@ -61,9 +61,9 @@ define('categorySelector', [ options = options || {}; options.onSelect = options.onSelect || function () {}; options.onSubmit = options.onSubmit || function () {}; - app.parseAndTranslate('admin/partials/categories/select-category', {}, function (html) { + app.parseAndTranslate('admin/partials/categories/select-category', { message: options.message }, function (html) { var modal = bootbox.dialog({ - title: '[[modules:composer.select_category]]', + title: options.title || '[[modules:composer.select_category]]', message: html, buttons: { save: { diff --git a/src/categories/create.js b/src/categories/create.js index 172ba7b1a6..7b787a666e 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -199,13 +199,19 @@ module.exports = function (Categories) { cache.del(`cid:${toCid}:tag:whitelist`); } - Categories.copyPrivilegesFrom = async function (fromCid, toCid, group) { + Categories.copyPrivilegesFrom = async function (fromCid, toCid, group, filter = []) { group = group || ''; + let privsToCopy; + if (group) { + privsToCopy = privileges.categories.groupPrivilegeList.slice(...filter); + } else { + const privs = privileges.categories.privilegeList.slice(); + const halfIdx = privs.length / 2; + privsToCopy = privs.slice(0, halfIdx).slice(...filter).concat(privs.slice(halfIdx).slice(...filter)); + } const data = await plugins.hooks.fire('filter:categories.copyPrivilegesFrom', { - privileges: group ? - privileges.categories.groupPrivilegeList.slice() : - privileges.categories.privilegeList.slice(), + privileges: privsToCopy, fromCid: fromCid, toCid: toCid, group: group, diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 31ac606a08..5f1e9cb219 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -73,15 +73,15 @@ Categories.copyPrivilegesToChildren = async function (socket, data) { const children = result[0]; for (const child of children) { // eslint-disable-next-line no-await-in-loop - await copyPrivilegesToChildrenRecursive(data.cid, child, data.group); + await copyPrivilegesToChildrenRecursive(data.cid, child, data.group, data.filter); } }; -async function copyPrivilegesToChildrenRecursive(parentCid, category, group) { - await categories.copyPrivilegesFrom(parentCid, category.cid, group); +async function copyPrivilegesToChildrenRecursive(parentCid, category, group, filter) { + await categories.copyPrivilegesFrom(parentCid, category.cid, group, filter); for (const child of category.children) { // eslint-disable-next-line no-await-in-loop - await copyPrivilegesToChildrenRecursive(parentCid, child, group); + await copyPrivilegesToChildrenRecursive(parentCid, child, group, filter); } } @@ -90,7 +90,7 @@ Categories.copySettingsFrom = async function (socket, data) { }; Categories.copyPrivilegesFrom = async function (socket, data) { - await categories.copyPrivilegesFrom(data.fromCid, data.toCid, data.group); + await categories.copyPrivilegesFrom(data.fromCid, data.toCid, data.group, data.filter); }; Categories.copyPrivilegesToAllCategories = async function (socket, data) { @@ -98,6 +98,6 @@ Categories.copyPrivilegesToAllCategories = async function (socket, data) { cids = cids.filter(cid => parseInt(cid, 10) !== parseInt(data.cid, 10)); for (const toCid of cids) { // eslint-disable-next-line no-await-in-loop - await categories.copyPrivilegesFrom(data.cid, toCid, data.group); + await categories.copyPrivilegesFrom(data.cid, toCid, data.group, data.filter); } }; diff --git a/src/views/admin/partials/categories/select-category.tpl b/src/views/admin/partials/categories/select-category.tpl index 7f8ed6c2b5..a0a28b6eed 100644 --- a/src/views/admin/partials/categories/select-category.tpl +++ b/src/views/admin/partials/categories/select-category.tpl @@ -19,4 +19,7 @@ - \ No newline at end of file + +{{{ if message }}} +
{message}
+{{{ end }}} \ No newline at end of file diff --git a/src/views/admin/partials/privileges/category.tpl b/src/views/admin/partials/privileges/category.tpl index f2d607308f..c2c03c2bec 100644 --- a/src/views/admin/partials/privileges/category.tpl +++ b/src/views/admin/partials/privileges/category.tpl @@ -2,21 +2,14 @@ - - - - - - - @@ -92,21 +85,14 @@
- [[admin/manage/categories:privileges.section-viewing]] + + + + + + + - [[admin/manage/categories:privileges.section-posting]] - - [[admin/manage/categories:privileges.section-moderation]] - - [[admin/manage/categories:privileges.section-other]] -
[[admin/manage/categories:privileges.section-group]]
- - - - - - -
- [[admin/manage/categories:privileges.section-viewing]] - - [[admin/manage/categories:privileges.section-posting]] - - [[admin/manage/categories:privileges.section-moderation]] - - [[admin/manage/categories:privileges.section-other]] + + + + + + +
[[admin/manage/categories:privileges.section-user]]