From 240aa9e83b1b91aec647331adc332a886ef777bc Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 27 May 2016 12:42:19 +0930 Subject: [PATCH] Improve permissions page - Introduce the concept of "required permissions" - basically a permission dependency tree. In order for a group to be granted one permission, they must also have another. - Improve redraw performance by not building dropdown menu contents until dropdown is opened ref #904 --- js/admin/dist/app.js | 167 +++++++++++++----- js/admin/src/app.js | 16 ++ js/admin/src/components/PermissionDropdown.js | 116 +++++++----- js/admin/src/components/PermissionGrid.js | 2 +- js/forum/dist/app.js | 26 ++- js/lib/components/Dropdown.js | 25 ++- less/admin/PermissionsPage.less | 20 +-- 7 files changed, 257 insertions(+), 115 deletions(-) diff --git a/js/admin/dist/app.js b/js/admin/dist/app.js index 3f666d9e4..3eff28f7d 100644 --- a/js/admin/dist/app.js +++ b/js/admin/dist/app.js @@ -16609,6 +16609,22 @@ System.register('flarum/app', ['flarum/App', 'flarum/initializers/store', 'flaru app.extensionSettings = {}; + app.getRequiredPermissions = function (permission) { + var required = []; + + if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) { + required.push('viewDiscussions'); + } + if (permission === 'discussion.delete') { + required.push('discussion.hide'); + } + if (permission === 'discussion.deletePosts') { + required.push('discussion.editPosts'); + } + + return required; + }; + _export('default', app); } }; @@ -18080,13 +18096,18 @@ System.register('flarum/components/Dropdown', ['flarum/Component', 'flarum/helpe } babelHelpers.createClass(Dropdown, [{ + key: 'init', + value: function init() { + this.showing = false; + } + }, { key: 'view', value: function view() { var items = this.props.children ? listItems(this.props.children) : []; return m( 'div', - { className: 'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length }, + { className: 'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length + (this.showing ? ' open' : '') }, this.getButton(), this.getMenu(items) ); @@ -18102,25 +18123,32 @@ System.register('flarum/components/Dropdown', ['flarum/Component', 'flarum/helpe // bottom of the viewport. If it does, we will apply class to make it show // above the toggle button instead of below it. this.$().on('shown.bs.dropdown', function () { + _this2.showing = true; + + if (_this2.props.onshow) { + _this2.props.onshow(); + } + + m.redraw(); + var $menu = _this2.$('.Dropdown-menu'); var isRight = $menu.hasClass('Dropdown-menu--right'); + $menu.removeClass('Dropdown-menu--top Dropdown-menu--right'); $menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()); $menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()); - - if (_this2.props.onshow) { - _this2.props.onshow(); - m.redraw(); - } }); this.$().on('hidden.bs.dropdown', function () { + _this2.showing = false; + if (_this2.props.onhide) { _this2.props.onhide(); - m.redraw(); } + + m.redraw(); }); } }, { @@ -19513,8 +19541,8 @@ System.register('flarum/components/Page', ['flarum/Component'], function (_expor });; 'use strict'; -System.register('flarum/components/PermissionDropdown', ['flarum/components/Dropdown', 'flarum/components/Button', 'flarum/components/Separator', 'flarum/models/Group', 'flarum/components/GroupBadge'], function (_export, _context) { - var Dropdown, Button, Separator, Group, GroupBadge, PermissionDropdown; +System.register('flarum/components/PermissionDropdown', ['flarum/components/Dropdown', 'flarum/components/Button', 'flarum/components/Separator', 'flarum/models/Group', 'flarum/components/Badge', 'flarum/components/GroupBadge'], function (_export, _context) { + var Dropdown, Button, Separator, Group, Badge, GroupBadge, PermissionDropdown; function badgeForId(id) { @@ -19523,6 +19551,30 @@ System.register('flarum/components/PermissionDropdown', ['flarum/components/Drop return group ? GroupBadge.component({ group: group, label: null }) : ''; } + function filterByRequiredPermissions(groupIds, permission) { + app.getRequiredPermissions(permission).forEach(function (required) { + var restrictToGroupIds = app.data.permissions[required] || []; + + if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) { + // do nothing + } else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) { + groupIds = groupIds.filter(function (id) { + return id !== Group.GUEST_ID; + }); + } else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) { + groupIds = restrictToGroupIds; + } else { + groupIds = restrictToGroupIds.filter(function (id) { + return groupIds.indexOf(id) !== -1; + }); + } + + groupIds = filterByRequiredPermissions(groupIds, required); + }); + + return groupIds; + } + return { setters: [function (_flarumComponentsDropdown) { Dropdown = _flarumComponentsDropdown.default; @@ -19532,6 +19584,8 @@ System.register('flarum/components/PermissionDropdown', ['flarum/components/Drop Separator = _flarumComponentsSeparator.default; }, function (_flarumModelsGroup) { Group = _flarumModelsGroup.default; + }, function (_flarumComponentsBadge) { + Badge = _flarumComponentsBadge.default; }, function (_flarumComponentsGroupBadge) { GroupBadge = _flarumComponentsGroupBadge.default; }], @@ -19552,56 +19606,64 @@ System.register('flarum/components/PermissionDropdown', ['flarum/components/Drop this.props.children = []; var groupIds = app.data.permissions[this.props.permission] || []; + + groupIds = filterByRequiredPermissions(groupIds, this.props.permission); + var everyone = groupIds.indexOf(Group.GUEST_ID) !== -1; var members = groupIds.indexOf(Group.MEMBER_ID) !== -1; var adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID); if (everyone) { - this.props.label = app.translator.trans('core.admin.permissions_controls.everyone_button'); + this.props.label = Badge.component({ icon: 'globe' }); } else if (members) { - this.props.label = app.translator.trans('core.admin.permissions_controls.members_button'); + this.props.label = Badge.component({ icon: 'user' }); } else { this.props.label = [badgeForId(Group.ADMINISTRATOR_ID), groupIds.map(badgeForId)]; } - if (this.props.allowGuest) { + if (this.showing) { + if (this.props.allowGuest) { + this.props.children.push(Button.component({ + children: [Badge.component({ icon: 'globe' }), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')], + icon: everyone ? 'check' : true, + onclick: function onclick() { + return _this2.save([Group.GUEST_ID]); + }, + disabled: this.isGroupDisabled(Group.GUEST_ID) + })); + } + this.props.children.push(Button.component({ - children: app.translator.trans('core.admin.permissions_controls.everyone_button'), - icon: everyone ? 'check' : true, + children: [Badge.component({ icon: 'user' }), ' ', app.translator.trans('core.admin.permissions_controls.members_button')], + icon: members ? 'check' : true, onclick: function onclick() { - return _this2.save([Group.GUEST_ID]); - } - })); - } - - this.props.children.push(Button.component({ - children: app.translator.trans('core.admin.permissions_controls.members_button'), - icon: members ? 'check' : true, - onclick: function onclick() { - return _this2.save([Group.MEMBER_ID]); - } - }), Separator.component(), Button.component({ - children: [GroupBadge.component({ group: adminGroup, label: null }), ' ', adminGroup.namePlural()], - icon: !everyone && !members ? 'check' : true, - disabled: !everyone && !members, - onclick: function onclick(e) { - if (e.shiftKey) e.stopPropagation(); - _this2.save([]); - } - })); - - [].push.apply(this.props.children, app.store.all('groups').filter(function (group) { - return [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1; - }).map(function (group) { - return Button.component({ - children: [GroupBadge.component({ group: group, label: null }), ' ', group.namePlural()], - icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true, + return _this2.save([Group.MEMBER_ID]); + }, + disabled: this.isGroupDisabled(Group.MEMBER_ID) + }), Separator.component(), Button.component({ + children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()], + icon: !everyone && !members ? 'check' : true, + disabled: !everyone && !members, onclick: function onclick(e) { if (e.shiftKey) e.stopPropagation(); - _this2.toggle(group.id()); + _this2.save([]); } - }); - })); + })); + + [].push.apply(this.props.children, app.store.all('groups').filter(function (group) { + return [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1; + }).map(function (group) { + return Button.component({ + children: [badgeForId(group.id()), ' ', group.namePlural()], + icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true, + onclick: function onclick(e) { + if (e.shiftKey) e.stopPropagation(); + _this2.toggle(group.id()); + }, + disabled: _this2.isGroupDisabled(group.id()) && _this2.isGroupDisabled(Group.MEMBER_ID) && _this2.isGroupDisabled(Group.GUEST_ID) + }); + })); + } return babelHelpers.get(Object.getPrototypeOf(PermissionDropdown.prototype), 'view', this).call(this); } @@ -19638,6 +19700,11 @@ System.register('flarum/components/PermissionDropdown', ['flarum/components/Drop this.save(groupIds); } + }, { + key: 'isGroupDisabled', + value: function isGroupDisabled(id) { + return filterByRequiredPermissions([id], this.props.permission).indexOf(id) === -1; + } }], [{ key: 'initProps', value: function initProps(props) { @@ -19749,7 +19816,7 @@ System.register('flarum/components/PermissionGrid', ['flarum/Component', 'flarum m( 'th', null, - child.icon ? icon(child.icon) : '', + icon(child.icon), child.label ), permissionCells(child), @@ -19871,10 +19938,10 @@ System.register('flarum/components/PermissionGrid', ['flarum/Component', 'flarum value: function moderateItems() { var items = new ItemList(); - items.add('viewPostIps', { + items.add('viewIpsPosts', { icon: 'bullseye', label: app.translator.trans('core.admin.permissions.view_post_ips_label'), - permission: 'discussion.viewPostIps' + permission: 'discussion.viewIpsPosts' }, 110); items.add('renameDiscussions', { @@ -23100,11 +23167,13 @@ System.register('flarum/utils/string', [], function (_export, _context) { _export('slug', slug); function getPlainContent(string) { - var dom = $('
').html(string.replace(/(<\/p>|
)/g, '$1  ')); + var html = string.replace(/(<\/p>|
)/g, '$1  ').replace(/]*>/ig, ' '); + + var dom = $('
').html(html); dom.find(getPlainContent.removeSelectors.join(',')).remove(); - return dom.text(); + return dom.text().replace(/\s+/g, ' ').trim(); } /** diff --git a/js/admin/src/app.js b/js/admin/src/app.js index 62ace9270..46a51aef5 100644 --- a/js/admin/src/app.js +++ b/js/admin/src/app.js @@ -14,4 +14,20 @@ app.initializers.add('boot', boot, -100); app.extensionSettings = {}; +app.getRequiredPermissions = function(permission) { + const required = []; + + if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) { + required.push('viewDiscussions'); + } + if (permission === 'discussion.delete') { + required.push('discussion.hide'); + } + if (permission === 'discussion.deletePosts') { + required.push('discussion.editPosts'); + } + + return required; +}; + export default app; diff --git a/js/admin/src/components/PermissionDropdown.js b/js/admin/src/components/PermissionDropdown.js index d61674392..425440ddf 100644 --- a/js/admin/src/components/PermissionDropdown.js +++ b/js/admin/src/components/PermissionDropdown.js @@ -2,6 +2,7 @@ import Dropdown from 'flarum/components/Dropdown'; import Button from 'flarum/components/Button'; import Separator from 'flarum/components/Separator'; import Group from 'flarum/models/Group'; +import Badge from 'flarum/components/Badge'; import GroupBadge from 'flarum/components/GroupBadge'; function badgeForId(id) { @@ -10,6 +11,27 @@ function badgeForId(id) { return group ? GroupBadge.component({group, label: null}) : ''; } +function filterByRequiredPermissions(groupIds, permission) { + app.getRequiredPermissions(permission) + .forEach(required => { + const restrictToGroupIds = app.data.permissions[required] || []; + + if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) { + // do nothing + } else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) { + groupIds = groupIds.filter(id => id !== Group.GUEST_ID); + } else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) { + groupIds = restrictToGroupIds; + } else { + groupIds = restrictToGroupIds.filter(id => groupIds.indexOf(id) !== -1); + } + + groupIds = filterByRequiredPermissions(groupIds, required); + }); + + return groupIds; +} + export default class PermissionDropdown extends Dropdown { static initProps(props) { super.initProps(props); @@ -21,15 +43,18 @@ export default class PermissionDropdown extends Dropdown { view() { this.props.children = []; - const groupIds = app.data.permissions[this.props.permission] || []; + let groupIds = app.data.permissions[this.props.permission] || []; + + groupIds = filterByRequiredPermissions(groupIds, this.props.permission); + const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1; const members = groupIds.indexOf(Group.MEMBER_ID) !== -1; const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID); if (everyone) { - this.props.label = app.translator.trans('core.admin.permissions_controls.everyone_button'); + this.props.label = Badge.component({icon: 'globe'}); } else if (members) { - this.props.label = app.translator.trans('core.admin.permissions_controls.members_button'); + this.props.label = Badge.component({icon: 'user'}); } else { this.props.label = [ badgeForId(Group.ADMINISTRATOR_ID), @@ -37,50 +62,55 @@ export default class PermissionDropdown extends Dropdown { ]; } - if (this.props.allowGuest) { + if (this.showing) { + if (this.props.allowGuest) { + this.props.children.push( + Button.component({ + children: [Badge.component({icon: 'globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')], + icon: everyone ? 'check' : true, + onclick: () => this.save([Group.GUEST_ID]), + disabled: this.isGroupDisabled(Group.GUEST_ID) + }) + ); + } + this.props.children.push( Button.component({ - children: app.translator.trans('core.admin.permissions_controls.everyone_button'), - icon: everyone ? 'check' : true, - onclick: () => this.save([Group.GUEST_ID]) + children: [Badge.component({icon: 'user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')], + icon: members ? 'check' : true, + onclick: () => this.save([Group.MEMBER_ID]), + disabled: this.isGroupDisabled(Group.MEMBER_ID) + }), + + Separator.component(), + + Button.component({ + children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()], + icon: !everyone && !members ? 'check' : true, + disabled: !everyone && !members, + onclick: e => { + if (e.shiftKey) e.stopPropagation(); + this.save([]); + } }) ); + + [].push.apply( + this.props.children, + app.store.all('groups') + .filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) + .map(group => Button.component({ + children: [badgeForId(group.id()), ' ', group.namePlural()], + icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true, + onclick: (e) => { + if (e.shiftKey) e.stopPropagation(); + this.toggle(group.id()); + }, + disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID) + })) + ); } - this.props.children.push( - Button.component({ - children: app.translator.trans('core.admin.permissions_controls.members_button'), - icon: members ? 'check' : true, - onclick: () => this.save([Group.MEMBER_ID]) - }), - - Separator.component(), - - Button.component({ - children: [GroupBadge.component({group: adminGroup, label: null}), ' ', adminGroup.namePlural()], - icon: !everyone && !members ? 'check' : true, - disabled: !everyone && !members, - onclick: e => { - if (e.shiftKey) e.stopPropagation(); - this.save([]); - } - }) - ); - - [].push.apply( - this.props.children, - app.store.all('groups') - .filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) - .map(group => Button.component({ - children: [GroupBadge.component({group, label: null}), ' ', group.namePlural()], - icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true, - onclick: (e) => { - if (e.shiftKey) e.stopPropagation(); - this.toggle(group.id()); - } - })) - ); - return super.view(); } @@ -112,4 +142,8 @@ export default class PermissionDropdown extends Dropdown { this.save(groupIds); } + + isGroupDisabled(id) { + return filterByRequiredPermissions([id], this.props.permission).indexOf(id) === -1; + } } diff --git a/js/admin/src/components/PermissionGrid.js b/js/admin/src/components/PermissionGrid.js index acb3ca39e..7d452e799 100644 --- a/js/admin/src/components/PermissionGrid.js +++ b/js/admin/src/components/PermissionGrid.js @@ -44,7 +44,7 @@ export default class PermissionGrid extends Component { {section.children.map(child => ( - {child.icon ? icon(child.icon) : ''}{child.label} + {icon(child.icon)}{child.label} {permissionCells(child)} diff --git a/js/forum/dist/app.js b/js/forum/dist/app.js index 9fd2bd127..d73c4a327 100644 --- a/js/forum/dist/app.js +++ b/js/forum/dist/app.js @@ -21457,13 +21457,18 @@ System.register('flarum/components/Dropdown', ['flarum/Component', 'flarum/helpe } babelHelpers.createClass(Dropdown, [{ + key: 'init', + value: function init() { + this.showing = false; + } + }, { key: 'view', value: function view() { var items = this.props.children ? listItems(this.props.children) : []; return m( 'div', - { className: 'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length }, + { className: 'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length + (this.showing ? ' open' : '') }, this.getButton(), this.getMenu(items) ); @@ -21479,25 +21484,32 @@ System.register('flarum/components/Dropdown', ['flarum/Component', 'flarum/helpe // bottom of the viewport. If it does, we will apply class to make it show // above the toggle button instead of below it. this.$().on('shown.bs.dropdown', function () { + _this2.showing = true; + + if (_this2.props.onshow) { + _this2.props.onshow(); + } + + m.redraw(); + var $menu = _this2.$('.Dropdown-menu'); var isRight = $menu.hasClass('Dropdown-menu--right'); + $menu.removeClass('Dropdown-menu--top Dropdown-menu--right'); $menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()); $menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()); - - if (_this2.props.onshow) { - _this2.props.onshow(); - m.redraw(); - } }); this.$().on('hidden.bs.dropdown', function () { + _this2.showing = false; + if (_this2.props.onhide) { _this2.props.onhide(); - m.redraw(); } + + m.redraw(); }); } }, { diff --git a/js/lib/components/Dropdown.js b/js/lib/components/Dropdown.js index 749f00141..8093ba506 100644 --- a/js/lib/components/Dropdown.js +++ b/js/lib/components/Dropdown.js @@ -29,11 +29,15 @@ export default class Dropdown extends Component { props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'caret-down'; } + init() { + this.showing = false; + } + view() { const items = this.props.children ? listItems(this.props.children) : []; return ( -
+
{this.getButton()} {this.getMenu(items)}
@@ -47,8 +51,17 @@ export default class Dropdown extends Component { // bottom of the viewport. If it does, we will apply class to make it show // above the toggle button instead of below it. this.$().on('shown.bs.dropdown', () => { + this.showing = true; + + if (this.props.onshow) { + this.props.onshow(); + } + + m.redraw(); + const $menu = this.$('.Dropdown-menu'); const isRight = $menu.hasClass('Dropdown-menu--right'); + $menu.removeClass('Dropdown-menu--top Dropdown-menu--right'); $menu.toggleClass( @@ -60,18 +73,16 @@ export default class Dropdown extends Component { 'Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width() ); - - if (this.props.onshow) { - this.props.onshow(); - m.redraw(); - } }); this.$().on('hidden.bs.dropdown', () => { + this.showing = false; + if (this.props.onhide) { this.props.onhide(); - m.redraw(); } + + m.redraw(); }); } diff --git a/less/admin/PermissionsPage.less b/less/admin/PermissionsPage.less index a2bc37263..c180b2afe 100644 --- a/less/admin/PermissionsPage.less +++ b/less/admin/PermissionsPage.less @@ -39,7 +39,7 @@ white-space: nowrap; td, th { - padding: 10px 0; + padding: 5px; text-align: left; } td { @@ -65,11 +65,6 @@ font-size: 14px; } } - tr:last-child { - td, th { - padding-bottom: 15px !important; - } - } .Dropdown { display: block; @@ -79,12 +74,15 @@ text-align: left; float: none; } + .Dropdown-menu { + margin: 0; + } } .Button { text-decoration: none; .Badge { - margin: -3px 0; + margin: -3px 2px -3px 0; vertical-align: 0; } } @@ -113,15 +111,17 @@ } .PermissionGrid-section { td, th { - padding-top: 15px; - border-top: 1px solid @control-bg; + padding-top: 20px; } } .PermissionGrid-child { td, th { - padding: 5px 0; + position: relative; } th { font-weight: normal; } + &:hover { + background: lighten(@control-bg, 3%); + } }