diff --git a/framework/core/js/admin/src/components/PermissionsPage.js b/framework/core/js/admin/src/components/PermissionsPage.js new file mode 100644 index 000000000..e594df3a8 --- /dev/null +++ b/framework/core/js/admin/src/components/PermissionsPage.js @@ -0,0 +1,269 @@ +import Component from 'flarum/Component'; +import Badge from 'flarum/components/Badge'; +import Select from 'flarum/components/Select'; +import Button from 'flarum/components/Button'; +import Group from 'flarum/models/Group'; +import icon from 'flarum/helpers/icon'; +import ItemList from 'flarum/utils/ItemList'; + +export default class PermissionsPage extends Component { + constructor(...args) { + super(...args); + + this.groups = app.store.all('groups') + .filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(Number(group.id())) === -1); + + this.permissions = this.permissionItems().toArray(); + this.scopes = this.scopeItems().toArray(); + this.scopeControls = this.scopeControlItems().toArray(); + } + + view() { + const permissionCells = permission => { + return this.scopes.map(scope => ( + + {scope.render(permission)} + + )); + }; + + return ( +
+
+
+ {this.groups.map(group => ( + + ))} + +
+
+ +
+
+ + + + + {this.scopes.map(scope => )} + + + + {this.permissions.map(section => ( + + + + {permissionCells(section)} + + {section.children.map(child => ( + + + {permissionCells(child)} + + ))} + + ))} +
{scope.label}{this.scopeControls}
{section.label} +
{child.label} +
+
+
+
+ ); + } + + permissionItems() { + const items = new ItemList(); + + items.add('view', { + label: 'View the forum', + children: this.viewItems().toArray() + }); + + items.add('start', { + label: 'Start discussions', + children: this.startItems().toArray() + }); + + items.add('reply', { + label: 'Reply to discussions', + children: this.replyItems().toArray() + }); + + items.add('moderate', { + label: 'Moderate', + children: this.moderateItems().toArray() + }); + + return items; + } + + viewItems() { + const items = new ItemList(); + + items.add('view', { + label: 'View discussions', + permission: 'forum.view', + allowGuest: true + }); + + items.add('signUp', { + label: 'Sign up', + setting: Select.component({options: ['Open']}) + }); + + return items; + } + + startItems() { + const items = new ItemList(); + + items.add('start', { + label: 'Start discussions', + permission: 'forum.startDiscussion' + }); + + items.add('allowRenaming', { + label: 'Allow renaming', + setting: Select.component({options: ['Indefinitely']}) + }); + + return items; + } + + replyItems() { + const items = new ItemList(); + + items.add('reply', { + label: 'Reply to discussions', + permission: 'discussion.reply' + }); + + items.add('allowPostEditing', { + label: 'Allow post editing', + setting: Select.component({options: ['Indefinitely']}) + }); + + return items; + } + + moderateItems() { + const items = new ItemList(); + + items.add('editPosts', { + label: 'Edit posts', + permission: 'discussion.editPosts' + }); + + items.add('deletePosts', { + label: 'Delete posts', + permission: 'discussion.deletePosts' + }); + + items.add('renameDiscussions', { + label: 'Rename discussions', + permission: 'discussion.rename' + }); + + items.add('deleteDiscussions', { + label: 'Delete discussions', + permission: 'discussion.delete' + }); + + items.add('suspendUsers', { + label: 'Suspend users', + permission: 'user.suspend' + }); + + return items; + } + + scopeItems() { + const items = new ItemList(); + + const groupBadge = id => { + const group = app.store.getById('groups', id); + + return Badge.component({ + icon: group.icon(), + style: {backgroundColor: group.color()}, + label: group.namePlural() + }); + }; + + const groupBadges = groupIds => { + let content; + + if (groupIds.indexOf(String(Group.GUEST_ID)) !== -1) { + content = 'Everyone'; + } else if (groupIds.indexOf(String(Group.MEMBER_ID)) !== -1) { + content = 'Members'; + } else { + content = [ + groupBadge(Group.ADMINISTRATOR_ID), + groupIds.map(groupBadge) + ]; + } + + return ( + + ); + }; + + items.add('global', { + label: 'Global', + render: permission => { + if (permission.setting) { + return permission.setting; + } else if (permission.permission) { + const groupIds = app.forum.attribute('permissions')[permission.permission] || []; + + return groupBadges(groupIds); + } + + return ''; + } + }); + + items.add('tag1', { + label: 'Blog', + render: permission => { + if (permission.setting) { + return ''; + } else if (permission.permission) { + const groupIds = app.forum.attribute('permissions')[permission.permission] || []; + + return groupBadges(groupIds); + } + + return ''; + } + }); + + return items; + } + + scopeControlItems() { + const items = new ItemList(); + + items.add('addTag', Button.component({ + children: 'Restrict by Tag', + icon: 'plus', + className: 'Button Button--text' + })) + + return items; + } +} diff --git a/framework/core/js/lib/components/Badge.js b/framework/core/js/lib/components/Badge.js index 929e22e2b..6e071a6bf 100644 --- a/framework/core/js/lib/components/Badge.js +++ b/framework/core/js/lib/components/Badge.js @@ -38,6 +38,6 @@ export default class Badge extends Component { config(isInitialized) { if (isInitialized) return; - this.$().tooltip(); + if (this.props.label) this.$().tooltip(); } } diff --git a/framework/core/js/lib/components/Select.js b/framework/core/js/lib/components/Select.js index 0974f857e..ab6ef9fea 100644 --- a/framework/core/js/lib/components/Select.js +++ b/framework/core/js/lib/components/Select.js @@ -15,7 +15,7 @@ export default class Select extends Component { return ( - {Object.keys(options).map(key => )} {icon('sort', {className: 'Select-caret'})} diff --git a/framework/core/less/admin/PermissionsPage.less b/framework/core/less/admin/PermissionsPage.less new file mode 100644 index 000000000..70acd0125 --- /dev/null +++ b/framework/core/less/admin/PermissionsPage.less @@ -0,0 +1,86 @@ +.PermissionsPage-groups { + background: @control-bg; + padding: 30px 0; +} +.Group { + width: 90px; + display: inline-block; + text-align: center; + color: @text-color; + font-weight: bold; +} +.Group-name { + display: block; + margin-top: 5px; +} +.Group-icon { + font-size: 14px; +} +.Group--add { + border: 1px dashed @muted-color; + color: @muted-color; + width: auto; + margin-left: 10px; + font-weight: normal; +} + + +.PermissionsPage-permissions { + padding: 30px 0; +} + +.PermissionGrid { + td, th { + padding: 10px 0; + text-align: left; + } + td { + color: @muted-color; + } + thead th { + text-transform: uppercase; + font-weight: bold; + font-size: 12px; + color: @muted-color; + width: 140px; + } + tbody { + th { + padding-right: 50px; + } + tr:last-child { + td, th { + padding-bottom: 15px !important; + } + } + select { + background: transparent; + padding: 0 30px 0 0; + height: auto; + border: 0; + width: 100%; + } + .Button { + text-decoration: none; + } + td:not(:hover) { + .Select-caret, .GroupsButton-caret { + display: none; + } + } + } +} +.PermissionGrid-section { + td, th { + padding-top: 15px; + border-top: 1px solid @control-bg; + } +} +.PermissionGrid-child { + td, th { + padding: 5px 0; + } + th { + font-weight: normal; + } +} diff --git a/framework/core/less/lib/Button.less b/framework/core/less/lib/Button.less index 3660c16bf..b16ab81d4 100755 --- a/framework/core/less/lib/Button.less +++ b/framework/core/less/lib/Button.less @@ -156,7 +156,7 @@ .Button--text { background: transparent !important; padding: 0; - color: @muted-color !important; + color: inherit !important; &:hover { text-decoration: underline; diff --git a/framework/core/src/Api/Serializers/GroupSerializer.php b/framework/core/src/Api/Serializers/GroupSerializer.php index 57e5085bc..62b9f4b25 100644 --- a/framework/core/src/Api/Serializers/GroupSerializer.php +++ b/framework/core/src/Api/Serializers/GroupSerializer.php @@ -13,11 +13,18 @@ class GroupSerializer extends Serializer protected function getDefaultAttributes($group) { return [ - 'id' => (int) $group->id, 'nameSingular' => $group->name_singular, 'namePlural' => $group->name_plural, 'color' => $group->color, 'icon' => $group->icon, ]; } + + /** + * @return callable + */ + protected function permissions() + { + return $this->hasMany('Flarum\Api\Serializers\PermissionSerializer'); + } } diff --git a/framework/core/src/Core/Groups/Group.php b/framework/core/src/Core/Groups/Group.php index ec505ea9f..96c0f6dc8 100755 --- a/framework/core/src/Core/Groups/Group.php +++ b/framework/core/src/Core/Groups/Group.php @@ -36,4 +36,14 @@ class Group extends Model { return $this->belongsToMany('Flarum\Core\Users\User', 'users_groups'); } + + /** + * Define the relationship with the group's permissions. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function permissions() + { + return $this->hasMany('Flarum\Core\Groups\Permission'); + } } diff --git a/framework/core/src/Core/Groups/Permission.php b/framework/core/src/Core/Groups/Permission.php new file mode 100755 index 000000000..237762566 --- /dev/null +++ b/framework/core/src/Core/Groups/Permission.php @@ -0,0 +1,55 @@ +belongsTo('Flarum\Core\Groups\Group', 'group_id'); + } + + /** + * Set the keys for a save update query. + * + * @param Builder $query + * @return Builder + */ + protected function setKeysForSaveQuery(Builder $query) + { + $query->where('group_id', $this->group_id) + ->where('permission', $this->permission); + + return $query; + } + + /** + * Get a map of permissions to the group IDs that have them. + * + * @return array[] + */ + public static function map() + { + $permissions = []; + + foreach (static::get() as $permission) { + $permissions[$permission->permission][] = $permission->group_id; + } + + return $permissions; + } +}