diff --git a/app/Auth/Queries/AllRolesPaginatedAndSorted.php b/app/Auth/Queries/AllRolesPaginatedAndSorted.php new file mode 100644 index 000000000..add1e9e54 --- /dev/null +++ b/app/Auth/Queries/AllRolesPaginatedAndSorted.php @@ -0,0 +1,37 @@ +<?php + +namespace BookStack\Auth\Queries; + +use BookStack\Auth\Role; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * Get all the roles in the system in a paginated format. + */ +class AllRolesPaginatedAndSorted +{ + /** + * @param array{sort: string, order: string, search: string} $sortData + */ + public function run(int $count, array $sortData): LengthAwarePaginator + { + $sort = $sortData['sort']; + if ($sort === 'created_at') { + $sort = 'users.created_at'; + } + + $query = Role::query()->select(['*']) + ->withCount(['users', 'permissions']) + ->orderBy($sort, $sortData['order']); + + if ($sortData['search']) { + $term = '%' . $sortData['search'] . '%'; + $query->where(function ($query) use ($term) { + $query->where('display_name', 'like', $term) + ->orWhere('description', 'like', $term); + }); + } + + return $query->paginate($count); + } +} diff --git a/app/Auth/Role.php b/app/Auth/Role.php index 17a4edcc0..b293d1af2 100644 --- a/app/Auth/Role.php +++ b/app/Auth/Role.php @@ -110,14 +110,6 @@ class Role extends Model implements Loggable return static::query()->where('system_name', '=', $systemName)->first(); } - /** - * Get all visible roles. - */ - public static function visible(): Collection - { - return static::query()->where('hidden', '=', false)->orderBy('name')->get(); - } - /** * {@inheritdoc} */ diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index fee31ffbf..d022bf35d 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Controllers; use BookStack\Auth\Permissions\PermissionsRepo; +use BookStack\Auth\Queries\AllRolesPaginatedAndSorted; use BookStack\Auth\Role; use BookStack\Exceptions\PermissionsException; use Exception; @@ -11,11 +12,8 @@ use Illuminate\Validation\ValidationException; class RoleController extends Controller { - protected $permissionsRepo; + protected PermissionsRepo $permissionsRepo; - /** - * PermissionController constructor. - */ public function __construct(PermissionsRepo $permissionsRepo) { $this->permissionsRepo = $permissionsRepo; @@ -24,14 +22,25 @@ class RoleController extends Controller /** * Show a listing of the roles in the system. */ - public function index() + public function index(Request $request) { $this->checkPermission('user-roles-manage'); - $roles = $this->permissionsRepo->getAllRoles(); + + $listDetails = [ + 'search' => $request->get('search', ''), + 'sort' => setting()->getForCurrentUser('roles_sort', 'display_name'), + 'order' => setting()->getForCurrentUser('roles_sort_order', 'asc'), + ]; + + $roles = (new AllRolesPaginatedAndSorted())->run(20, $listDetails); + $roles->appends(['search' => $listDetails['search']]); $this->setPageTitle(trans('settings.roles')); - return view('settings.roles.index', ['roles' => $roles]); + return view('settings.roles.index', [ + 'roles' => $roles, + 'listDetails' => $listDetails, + ]); } /** @@ -75,16 +84,11 @@ class RoleController extends Controller /** * Show the form for editing a user role. - * - * @throws PermissionsException */ public function edit(string $id) { $this->checkPermission('user-roles-manage'); $role = $this->permissionsRepo->getRoleById($id); - if ($role->hidden) { - throw new PermissionsException(trans('errors.role_cannot_be_edited')); - } $this->setPageTitle(trans('settings.role_edit')); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 9b089c29a..bd69aa8f5 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -251,7 +251,7 @@ class UserController extends Controller */ public function changeSort(Request $request, string $id, string $type) { - $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users']; + $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles']; if (!in_array($type, $validSortTypes)) { return redirect()->back(500); } @@ -318,7 +318,13 @@ class UserController extends Controller $this->checkPermissionOrCurrentUser('users-manage', $userId); $sort = $request->get('sort'); - if (!in_array($sort, ['name', 'created_at', 'updated_at', 'default', 'email', 'last_activity_at'])) { + // TODO - Need to find a better way to validate sort options + // Probably better to do a simple validation here then validate at usage. + $validSorts = [ + 'name', 'created_at', 'updated_at', 'default', 'email', 'last_activity_at', 'display_name', + 'users_count', 'permissions_count', + ]; + if (!in_array($sort, $validSorts)) { $sort = 'name'; } diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index d4d6d3bae..e8978d41e 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -133,6 +133,11 @@ return [ // Role Settings 'roles' => 'Roles', 'role_user_roles' => 'User Roles', + 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', + 'roles_x_users_assigned' => '1 user assigned|:count users assigned', + 'roles_x_permissions_provided' => '1 permission|:count permissions', + 'roles_assigned_users' => 'Assigned Users', + 'roles_permissions_provided' => 'Provided Permissions', 'role_create' => 'Create New Role', 'role_create_success' => 'Role successfully created', 'role_delete' => 'Delete Role', diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php index 4c3b5625a..6aeb16f92 100644 --- a/resources/views/settings/roles/index.blade.php +++ b/resources/views/settings/roles/index.blade.php @@ -12,30 +12,38 @@ <h1 class="list-heading">{{ trans('settings.role_user_roles') }}</h1> <div class="text-right"> - <a href="{{ url("/settings/roles/new") }}" class="button outline">{{ trans('settings.role_create') }}</a> + <a href="{{ url("/settings/roles/new") }}" class="button outline my-none">{{ trans('settings.role_create') }}</a> </div> </div> - <table class="table"> - <tr> - <th>{{ trans('settings.role_name') }}</th> - <th></th> - <th class="text-center">{{ trans('settings.users') }}</th> - </tr> - @foreach($roles as $role) - <tr> - <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td> - <td> - @if($role->mfa_enforced) - <span title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </span> - @endif - {{ $role->description }} - </td> - <td class="text-center">{{ $role->users->count() }}</td> - </tr> - @endforeach - </table> + <p class="text-muted">{{ trans('settings.roles_index_desc') }}</p> + <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap"> + <div> + <div class="block inline mr-xs"> + <form method="get" action="{{ url("/settings/roles") }}"> + <input type="text" name="search" placeholder="{{ trans('common.search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif> + </form> + </div> + </div> + <div class="justify-flex-end"> + @include('common.sort', ['options' => [ + 'display_name' => trans('common.sort_name'), + 'users_count' => trans('settings.roles_assigned_users'), + 'permissions_count' => trans('settings.roles_permissions_provided'), + ], 'order' => $listDetails['order'], 'sort' => $listDetails['sort'], 'type' => 'roles']) + </div> + </div> + + <div class="item-list"> + @foreach($roles as $role) + @include('settings.roles.parts.roles-list-item', ['role' => $role]) + @endforeach + </div> + + <div class="mb-m"> + {{ $roles->links() }} + </div> </div> </div> diff --git a/resources/views/settings/roles/parts/roles-list-item.blade.php b/resources/views/settings/roles/parts/roles-list-item.blade.php new file mode 100644 index 000000000..43e8dc81a --- /dev/null +++ b/resources/views/settings/roles/parts/roles-list-item.blade.php @@ -0,0 +1,14 @@ +<div class="item-list-row flex-container-row py-xs items-center"> + <div class="py-xs px-m flex-2"> + <a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a><br> + @if($role->mfa_enforced) + <small title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </small> + @endif + <small>{{ $role->description }}</small> + </div> + <div class="text-right flex py-xs px-m text-muted"> + {{ trans_choice('settings.roles_x_users_assigned', $role->users_count, ['count' => $role->users_count]) }} + <br> + {{ trans_choice('settings.roles_x_permissions_provided', $role->permissions_count, ['count' => $role->permissions_count]) }} + </div> +</div> \ No newline at end of file diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 5fda0f6c0..139ac4579 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -10,7 +10,7 @@ <div class="flex-container-row wrap justify-space-between items-center"> <h1 class="list-heading">{{ trans('settings.users') }}</h1> <div> - <a href="{{ url("/settings/users/create") }}" class="outline button mt-none">{{ trans('settings.users_add_new') }}</a> + <a href="{{ url("/settings/users/create") }}" class="outline button my-none">{{ trans('settings.users_add_new') }}</a> </div> </div>