diff --git a/framework/core/js/src/admin/components/EditGroupModal.js b/framework/core/js/src/admin/components/EditGroupModal.js index 12d04c68d..705314fe6 100644 --- a/framework/core/js/src/admin/components/EditGroupModal.js +++ b/framework/core/js/src/admin/components/EditGroupModal.js @@ -3,6 +3,7 @@ import Button from '../../common/components/Button'; import Badge from '../../common/components/Badge'; import Group from '../../common/models/Group'; import ItemList from '../../common/utils/ItemList'; +import Switch from '../../common/components/Switch'; /** * The `EditGroupModal` component shows a modal dialog which allows the user @@ -16,6 +17,7 @@ export default class EditGroupModal extends Modal { this.namePlural = m.prop(this.group.namePlural() || ''); this.icon = m.prop(this.group.icon() || ''); this.color = m.prop(this.group.color() || ''); + this.isHidden = m.prop(this.group.isHidden() || false); } className() { @@ -89,6 +91,18 @@ export default class EditGroupModal extends Modal { 10 ); + items.add( + 'hidden', +
+ {Switch.component({ + state: !!Number(this.isHidden()), + children: app.translator.trans('core.admin.edit_group.hide_label'), + onchange: this.isHidden, + })} +
, + 10 + ); + items.add( 'submit',
@@ -118,6 +132,7 @@ export default class EditGroupModal extends Modal { namePlural: this.namePlural(), color: this.color(), icon: this.icon(), + isHidden: this.isHidden(), }; } diff --git a/framework/core/js/src/admin/components/PermissionGrid.js b/framework/core/js/src/admin/components/PermissionGrid.js index b81d92faa..b2326d6bd 100644 --- a/framework/core/js/src/admin/components/PermissionGrid.js +++ b/framework/core/js/src/admin/components/PermissionGrid.js @@ -112,6 +112,16 @@ export default class PermissionGrid extends Component { 100 ); + items.add( + 'viewHiddenGroups', + { + icon: 'fas fa-users', + label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'), + permission: 'viewHiddenGroups', + }, + 100 + ); + items.add( 'viewUserList', { diff --git a/framework/core/js/src/common/models/Group.js b/framework/core/js/src/common/models/Group.js index ad3017ff8..46087032a 100644 --- a/framework/core/js/src/common/models/Group.js +++ b/framework/core/js/src/common/models/Group.js @@ -7,6 +7,7 @@ Object.assign(Group.prototype, { namePlural: Model.attribute('namePlural'), color: Model.attribute('color'), icon: Model.attribute('icon'), + isHidden: Model.attribute('isHidden'), }); Group.ADMINISTRATOR_ID = '1'; diff --git a/framework/core/migrations/2020_04_21_130500_change_permission_groups_add_is_hidden.php b/framework/core/migrations/2020_04_21_130500_change_permission_groups_add_is_hidden.php new file mode 100644 index 000000000..3b84611b9 --- /dev/null +++ b/framework/core/migrations/2020_04_21_130500_change_permission_groups_add_is_hidden.php @@ -0,0 +1,14 @@ + ['boolean', 'default' => false] +]); diff --git a/framework/core/src/Api/Controller/ListGroupsController.php b/framework/core/src/Api/Controller/ListGroupsController.php index 62c93e27c..098f703d9 100644 --- a/framework/core/src/Api/Controller/ListGroupsController.php +++ b/framework/core/src/Api/Controller/ListGroupsController.php @@ -26,6 +26,8 @@ class ListGroupsController extends AbstractListController */ protected function data(ServerRequestInterface $request, Document $document) { - return Group::all(); + $actor = $request->getAttribute('actor'); + + return Group::whereVisibleTo($actor)->get(); } } diff --git a/framework/core/src/Api/Serializer/BasicUserSerializer.php b/framework/core/src/Api/Serializer/BasicUserSerializer.php index 851b7833b..6fb6c24d0 100644 --- a/framework/core/src/Api/Serializer/BasicUserSerializer.php +++ b/framework/core/src/Api/Serializer/BasicUserSerializer.php @@ -45,6 +45,10 @@ class BasicUserSerializer extends AbstractSerializer */ protected function groups($user) { - return $this->hasMany($user, GroupSerializer::class); + if ($this->getActor()->can('viewHiddenGroups')) { + return $this->hasMany($user, GroupSerializer::class); + } + + return $this->hasMany($user, GroupSerializer::class, 'visibleGroups'); } } diff --git a/framework/core/src/Api/Serializer/GroupSerializer.php b/framework/core/src/Api/Serializer/GroupSerializer.php index c0f743dab..7675adc46 100644 --- a/framework/core/src/Api/Serializer/GroupSerializer.php +++ b/framework/core/src/Api/Serializer/GroupSerializer.php @@ -52,6 +52,7 @@ class GroupSerializer extends AbstractSerializer 'namePlural' => $this->translateGroupName($group->name_plural), 'color' => $group->color, 'icon' => $group->icon, + 'isHidden' => $group->is_hidden ]; } diff --git a/framework/core/src/Group/Command/CreateGroupHandler.php b/framework/core/src/Group/Command/CreateGroupHandler.php index 0efd8fc8a..a55a56349 100644 --- a/framework/core/src/Group/Command/CreateGroupHandler.php +++ b/framework/core/src/Group/Command/CreateGroupHandler.php @@ -54,7 +54,8 @@ class CreateGroupHandler Arr::get($data, 'attributes.nameSingular'), Arr::get($data, 'attributes.namePlural'), Arr::get($data, 'attributes.color'), - Arr::get($data, 'attributes.icon') + Arr::get($data, 'attributes.icon'), + Arr::get($data, 'attributes.isHidden', false) ); $this->events->dispatch( diff --git a/framework/core/src/Group/Command/EditGroupHandler.php b/framework/core/src/Group/Command/EditGroupHandler.php index ef357c41e..213cafb87 100644 --- a/framework/core/src/Group/Command/EditGroupHandler.php +++ b/framework/core/src/Group/Command/EditGroupHandler.php @@ -74,6 +74,10 @@ class EditGroupHandler $group->icon = $attributes['icon']; } + if (isset($attributes['isHidden'])) { + $group->is_hidden = $attributes['isHidden']; + } + $this->events->dispatch( new Saving($group, $actor, $data) ); diff --git a/framework/core/src/Group/Group.php b/framework/core/src/Group/Group.php index bc7e8dedf..b64a1ed26 100644 --- a/framework/core/src/Group/Group.php +++ b/framework/core/src/Group/Group.php @@ -23,6 +23,7 @@ use Flarum\User\User; * @property string $name_plural * @property string|null $color * @property string|null $icon + * @property bool $is_hidden * @property \Illuminate\Database\Eloquent\Collection $users * @property \Illuminate\Database\Eloquent\Collection $permissions */ @@ -72,9 +73,10 @@ class Group extends AbstractModel * @param string $namePlural * @param string $color * @param string $icon + * @param bool $isHidden * @return static */ - public static function build($nameSingular, $namePlural, $color, $icon) + public static function build($nameSingular, $namePlural, $color = null, $icon = null, bool $isHidden = false): self { $group = new static; @@ -82,6 +84,7 @@ class Group extends AbstractModel $group->name_plural = $namePlural; $group->color = $color; $group->icon = $icon; + $group->is_hidden = $isHidden; $group->raise(new Created($group)); diff --git a/framework/core/src/Group/GroupPolicy.php b/framework/core/src/Group/GroupPolicy.php index e3f6d0874..232d8bb0d 100644 --- a/framework/core/src/Group/GroupPolicy.php +++ b/framework/core/src/Group/GroupPolicy.php @@ -11,6 +11,7 @@ namespace Flarum\Group; use Flarum\User\AbstractPolicy; use Flarum\User\User; +use Illuminate\Database\Eloquent\Builder; class GroupPolicy extends AbstractPolicy { @@ -30,4 +31,15 @@ class GroupPolicy extends AbstractPolicy return true; } } + + /** + * @param User $actor + * @param Builder $query + */ + public function find(User $actor, Builder $query) + { + if ($actor->cannot('viewHiddenGroups')) { + $query->where('is_hidden', false); + } + } } diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index 197657447..8fc28e74c 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -606,6 +606,11 @@ class User extends AbstractModel return $this->belongsToMany(Group::class); } + public function visibleGroups() + { + return $this->belongsToMany(Group::class)->where('is_hidden', false); + } + /** * Define the relationship with the user's notifications. * diff --git a/framework/core/tests/integration/api/groups/ListTest.php b/framework/core/tests/integration/api/groups/ListTest.php index 591df53ca..ddcc47e96 100644 --- a/framework/core/tests/integration/api/groups/ListTest.php +++ b/framework/core/tests/integration/api/groups/ListTest.php @@ -9,15 +9,37 @@ namespace Flarum\Tests\integration\api\groups; -use Flarum\Group\Group; +use Flarum\Tests\integration\RetrievesAuthorizedUsers; use Flarum\Tests\integration\TestCase; +use Illuminate\Support\Arr; class ListTest extends TestCase { + use RetrievesAuthorizedUsers; + + public function setUp() + { + parent::setUp(); + + $this->prepareDatabase([ + 'users' => [ + $this->adminUser(), + $this->normalUser(), + ], + 'groups' => [ + $this->adminGroup(), + $this->hiddenGroup() + ], + 'group_user' => [ + ['user_id' => 1, 'group_id' => 1], + ], + ]); + } + /** * @test */ - public function shows_index_for_guest() + public function shows_limited_index_for_guest() { $response = $this->send( $this->request('GET', '/api/groups') @@ -26,6 +48,35 @@ class ListTest extends TestCase $this->assertEquals(200, $response->getStatusCode()); $data = json_decode($response->getBody()->getContents(), true); - $this->assertEquals(Group::count(), count($data['data'])); + $this->assertEquals(['1'], Arr::pluck($data['data'], 'id')); + } + + /** + * @test + */ + public function shows_index_for_admin() + { + $response = $this->send( + $this->request('GET', '/api/groups', [ + 'authenticatedAs' => 1, + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($response->getBody()->getContents(), true); + + $this->assertEquals(['1', '10'], Arr::pluck($data['data'], 'id')); + } + + protected function hiddenGroup(): array + { + return [ + 'id' => 10, + 'name_singular' => 'Hidden', + 'name_plural' => 'Ninjas', + 'color' => null, + 'icon' => 'fas fa-wrench', + 'is_hidden' => 1 + ]; } }