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
This commit is contained in:
Toby Zerner 2016-05-27 12:42:19 +09:30
parent 1177880483
commit 240aa9e83b
7 changed files with 257 additions and 115 deletions

167
js/admin/dist/app.js vendored
View File

@ -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 = $('<div/>').html(string.replace(/(<\/p>|<br>)/g, '$1 &nbsp;'));
var html = string.replace(/(<\/p>|<br>)/g, '$1 &nbsp;').replace(/<img\b[^>]*>/ig, ' ');
var dom = $('<div/>').html(html);
dom.find(getPlainContent.removeSelectors.join(',')).remove();
return dom.text();
return dom.text().replace(/\s+/g, ' ').trim();
}
/**

View File

@ -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;

View File

@ -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;
}
}

View File

@ -44,7 +44,7 @@ export default class PermissionGrid extends Component {
</tr>
{section.children.map(child => (
<tr className="PermissionGrid-child">
<th>{child.icon ? icon(child.icon) : ''}{child.label}</th>
<th>{icon(child.icon)}{child.label}</th>
{permissionCells(child)}
<td/>
</tr>

26
js/forum/dist/app.js vendored
View File

@ -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();
});
}
}, {

View File

@ -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 (
<div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length}>
<div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
{this.getButton()}
{this.getMenu(items)}
</div>
@ -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();
});
}

View File

@ -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%);
}
}