namespace BookStack\Actions\Queries;
use BookStack\Actions\Webhook;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
*/
class WebhooksAllPaginatedAndSorted
{
- /**
- * @param array{sort: string, order: string, search: string} $sortData
- */
- public function run(int $count, array $sortData): LengthAwarePaginator
+ public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
{
- $sort = $sortData['sort'];
-
$query = Webhook::query()->select(['*'])
->withCount(['trackedEvents'])
- ->orderBy($sort, $sortData['order']);
+ ->orderBy($listOptions->getSort(), $listOptions->getOrder());
- if ($sortData['search']) {
- $term = '%' . $sortData['search'] . '%';
+ if ($listOptions->getSearch()) {
+ $term = '%' . $listOptions->getSearch() . '%';
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('endpoint', 'like', $term);
namespace BookStack\Auth\Queries;
use BookStack\Auth\Role;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
*/
class RolesAllPaginatedAndSorted
{
- /**
- * @param array{sort: string, order: string, search: string} $sortData
- */
- public function run(int $count, array $sortData): LengthAwarePaginator
+ public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
{
- $sort = $sortData['sort'];
+ $sort = $listOptions->getSort();
if ($sort === 'created_at') {
$sort = 'users.created_at';
}
$query = Role::query()->select(['*'])
->withCount(['users', 'permissions'])
- ->orderBy($sort, $sortData['order']);
+ ->orderBy($sort, $listOptions->getOrder());
- if ($sortData['search']) {
- $term = '%' . $sortData['search'] . '%';
+ if ($listOptions->getSearch()) {
+ $term = '%' . $listOptions->getSearch() . '%';
$query->where(function ($query) use ($term) {
$query->where('display_name', 'like', $term)
->orWhere('description', 'like', $term);
namespace BookStack\Auth\Queries;
use BookStack\Auth\User;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
*/
class UsersAllPaginatedAndSorted
{
- /**
- * @param array{sort: string, order: string, search: string} $sortData
- */
- public function run(int $count, array $sortData): LengthAwarePaginator
+ public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
{
- $sort = $sortData['sort'];
+ $sort = $listOptions->getSort();
if ($sort === 'created_at') {
$sort = 'users.created_at';
}
->scopes(['withLastActivityAt'])
->with(['roles', 'avatar'])
->withCount('mfaValues')
- ->orderBy($sort, $sortData['order']);
+ ->orderBy($sort, $listOptions->getOrder());
- if ($sortData['search']) {
- $term = '%' . $sortData['search'] . '%';
+ if ($listOptions->getSearch()) {
+ $term = '%' . $listOptions->getSearch() . '%';
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('email', 'like', $term);
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\References\ReferenceFetcher;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
/**
* Display a listing of the book.
*/
- public function index()
+ public function index(Request $request)
{
$view = setting()->getForCurrentUser('books_view_type');
- $sort = setting()->getForCurrentUser('books_sort', 'name');
- $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+ $listOptions = SimpleListOptions::fromRequest($request, 'books')->withSortOptions([
+ 'name' => trans('common.sort_name'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ ]);
- $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
+ $books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
$recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
$popular = $this->bookRepo->getPopular(4);
$new = $this->bookRepo->getRecentlyCreated(4);
'popular' => $popular,
'new' => $new,
'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
+ 'listOptions' => $listOptions,
]);
}
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\References\ReferenceFetcher;
+use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
/**
* Display a listing of the book.
*/
- public function index()
+ public function index(Request $request)
{
$view = setting()->getForCurrentUser('bookshelves_view_type');
- $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
- $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
- $sortOptions = [
+ $listOptions = SimpleListOptions::fromRequest($request, 'bookshelves')->withSortOptions([
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
- ];
+ ]);
- $shelves = $this->shelfRepo->getAllPaginated(18, $sort, $order);
+ $shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
$recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
$popular = $this->shelfRepo->getPopular(4);
$new = $this->shelfRepo->getRecentlyCreated(4);
'popular' => $popular,
'new' => $new,
'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
- 'sortOptions' => $sortOptions,
+ 'listOptions' => $listOptions,
]);
}
*
* @throws NotFoundException
*/
- public function show(ActivityQueries $activities, string $slug)
+ public function show(Request $request, ActivityQueries $activities, string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-view', $shelf);
- $sort = setting()->getForCurrentUser('shelf_books_sort', 'default');
- $order = setting()->getForCurrentUser('shelf_books_sort_order', 'asc');
+ $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
+ 'default' => trans('common.sort_default'),
+ 'name' => trans('common.sort_name'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ ]);
+ $sort = $listOptions->getSort();
$sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
- ->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $order === 'desc')
+ ->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $listOptions->getOrder() === 'desc')
->values()
->all();
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
'view' => $view,
'activity' => $activities->entityActivity($shelf, 20, 1),
- 'order' => $order,
- 'sort' => $sort,
+ 'listOptions' => $listOptions,
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
]);
}
use BookStack\Auth\Queries\RolesAllPaginatedAndSorted;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
+use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
{
$this->checkPermission('user-roles-manage');
- $listDetails = [
- 'search' => $request->get('search', ''),
- 'sort' => setting()->getForCurrentUser('roles_sort', 'display_name'),
- 'order' => setting()->getForCurrentUser('roles_sort_order', 'asc'),
- ];
+ $listOptions = SimpleListOptions::fromRequest($request, 'roles')->withSortOptions([
+ 'display_name' => trans('common.sort_name'),
+ 'users_count' => trans('settings.roles_assigned_users'),
+ 'permissions_count' => trans('settings.roles_permissions_provided'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ ]);
- $roles = (new RolesAllPaginatedAndSorted())->run(20, $listDetails);
- $roles->appends(['search' => $listDetails['search']]);
+ $roles = (new RolesAllPaginatedAndSorted())->run(20, $listOptions);
+ $roles->appends($listOptions->getPaginationAppends());
$this->setPageTitle(trans('settings.roles'));
return view('settings.roles.index', [
'roles' => $roles,
- 'listDetails' => $listDetails,
+ 'listOptions' => $listOptions,
]);
}
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
+use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
public function index(Request $request)
{
$this->checkPermission('users-manage');
- $listDetails = [
- 'search' => $request->get('search', ''),
- 'sort' => setting()->getForCurrentUser('users_sort', 'name'),
- 'order' => setting()->getForCurrentUser('users_sort_order', 'asc'),
- ];
- $users = (new UsersAllPaginatedAndSorted())->run(20, $listDetails);
+ $listOptions = SimpleListOptions::fromRequest($request, 'users')->withSortOptions([
+ 'name' => trans('common.sort_name'),
+ 'email' => trans('auth.email'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ 'last_activity_at' => trans('settings.users_latest_activity'),
+ ]);
+
+ $users = (new UsersAllPaginatedAndSorted())->run(20, $listOptions);
$this->setPageTitle(trans('settings.users'));
- $users->appends(['search' => $listDetails['search']]);
+ $users->appends($listOptions->getPaginationAppends());
return view('users.index', [
'users' => $users,
- 'listDetails' => $listDetails,
+ 'listOptions' => $listOptions,
]);
}
return redirect()->back(500);
}
- return $this->changeListSort($id, $request, $type);
+ $this->checkPermissionOrCurrentUser('users-manage', $id);
+
+ $sort = substr($request->get('sort') ?: 'name', 0, 50);
+ $order = $request->get('order') === 'desc' ? 'desc' : 'asc';
+
+ $user = $this->userRepo->getById($id);
+ $sortKey = $type . '_sort';
+ $orderKey = $type . '_sort_order';
+ setting()->putUser($user, $sortKey, $sort);
+ setting()->putUser($user, $orderKey, $order);
+
+ return redirect()->back(302, [], "/settings/users/{$id}");
}
/**
setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
}
-
- /**
- * Changed the stored preference for a list sort order.
- */
- protected function changeListSort(int $userId, Request $request, string $listName)
- {
- $this->checkPermissionOrCurrentUser('users-manage', $userId);
-
- $sort = $request->get('sort');
- // 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', 'endpoint', 'active',
- ];
- if (!in_array($sort, $validSorts)) {
- $sort = 'name';
- }
-
- $order = $request->get('order');
- if (!in_array($order, ['asc', 'desc'])) {
- $order = 'asc';
- }
-
- $user = $this->userRepo->getById($userId);
- $sortKey = $listName . '_sort';
- $orderKey = $listName . '_sort_order';
- setting()->putUser($user, $sortKey, $sort);
- setting()->putUser($user, $orderKey, $order);
-
- return redirect()->back(302, [], "/settings/users/$userId");
- }
}
use BookStack\Actions\ActivityType;
use BookStack\Actions\Queries\WebhooksAllPaginatedAndSorted;
use BookStack\Actions\Webhook;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
class WebhookController extends Controller
*/
public function index(Request $request)
{
- $listDetails = [
- 'search' => $request->get('search', ''),
- 'sort' => setting()->getForCurrentUser('webhooks_sort', 'name'),
- 'order' => setting()->getForCurrentUser('webhooks_sort_order', 'asc'),
- ];
+ $listOptions = SimpleListOptions::fromRequest($request, 'webhooks')->withSortOptions([
+ 'name' => trans('common.sort_name'),
+ 'endpoint' => trans('settings.webhooks_endpoint'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ 'active' => trans('common.status'),
+ ]);
- $webhooks = (new WebhooksAllPaginatedAndSorted())->run(20, $listDetails);
- $webhooks->appends(['search' => $listDetails['search']]);
+ $webhooks = (new WebhooksAllPaginatedAndSorted())->run(20, $listOptions);
+ $webhooks->appends($listOptions->getPaginationAppends());
$this->setPageTitle(trans('settings.webhooks'));
return view('settings.webhooks.index', [
'webhooks' => $webhooks,
- 'listDetails' => $listDetails,
+ 'listOptions' => $listOptions,
]);
}
--- /dev/null
+<?php
+
+namespace BookStack\Util;
+
+use Illuminate\Http\Request;
+
+/**
+ * Handled options commonly used for item lists within the system, providing a standard
+ * model for handling and validating sort, order and search options.
+ */
+class SimpleListOptions
+{
+ protected string $typeKey;
+ protected string $sort;
+ protected string $order;
+ protected string $search;
+ protected array $sortOptions = [];
+
+ public function __construct(string $typeKey, string $sort, string $order, string $search = '')
+ {
+ $this->typeKey = $typeKey;
+ $this->sort = $sort;
+ $this->order = $order;
+ $this->search = $search;
+ }
+
+ /**
+ * Create a new instance from the given request.
+ * Takes the item type (plural) that's used as a key for storing sort preferences.
+ */
+ public static function fromRequest(Request $request, string $typeKey): self
+ {
+ $search = $request->get('search', '');
+ $sort = setting()->getForCurrentUser($typeKey . '_sort', '');
+ $order = setting()->getForCurrentUser($typeKey . '_sort_order', 'asc');
+
+ return new static($typeKey, $sort, $order, $search);
+ }
+
+ /**
+ * Configure the valid sort options for this set of list options.
+ * Provided sort options must be an array, keyed by search properties
+ * with values being user-visible option labels.
+ * Returns current options for easy fluent usage during creation.
+ */
+ public function withSortOptions(array $sortOptions): self
+ {
+ $this->sortOptions = array_merge($this->sortOptions, $sortOptions);
+
+ return $this;
+ }
+
+ /**
+ * Get the current order option.
+ */
+ public function getOrder(): string
+ {
+ return strtolower($this->order) === 'desc' ? 'desc' : 'asc';
+ }
+
+ /**
+ * Get the current sort option.
+ */
+ public function getSort(): string
+ {
+ $default = array_key_first($this->sortOptions) ?? 'name';
+ $sort = $this->sort ?: $default;
+
+ if (empty($this->sortOptions) || array_key_exists($sort, $this->sortOptions)) {
+ return $sort;
+ }
+
+ return $default;
+ }
+
+ /**
+ * Get the set search term.
+ */
+ public function getSearch(): string
+ {
+ return $this->search;
+ }
+
+ /**
+ * Get the data to append for pagination.
+ */
+ public function getPaginationAppends(): array
+ {
+ return ['search' => $this->search];
+ }
+
+ /**
+ * Get the data required by the sort control view.
+ */
+ public function getSortControlData(): array
+ {
+ return [
+ 'options' => $this->sortOptions,
+ 'order' => $this->getOrder(),
+ 'sort' => $this->getSort(),
+ 'type' => $this->typeKey,
+ ];
+ }
+}
@extends('layouts.tri')
@section('body')
- @include('books.parts.list', ['books' => $books, 'view' => $view])
+ @include('books.parts.list', ['books' => $books, 'view' => $view, 'listOptions' => $listOptions])
@stop
@section('left')
<div class="grid half v-center no-row-gap">
<h1 class="list-heading">{{ trans('entities.books') }}</h1>
<div class="text-m-right my-m">
-
- @include('common.sort', ['options' => [
- 'name' => trans('common.sort_name'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- ], 'order' => $order, 'sort' => $sort, 'type' => 'books'])
-
+ @include('common.sort', $listOptions->getSortControlData())
</div>
</div>
@if(count($books) > 0)
<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>
+ <input type="text"
+ name="search"
+ placeholder="{{ trans('common.search') }}"
+ value="{{ $listOptions->getSearch() }}">
</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'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- ], 'order' => $listDetails['order'], 'sort' => $listDetails['sort'], 'type' => 'roles'])
+ @include('common.sort', $listOptions->getSortControlData())
</div>
</div>
<div>
<div class="block inline mr-xs">
<form method="get" action="{{ url("/settings/webhooks") }}">
- <input type="text" name="search" placeholder="{{ trans('common.search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
+ <input type="text"
+ name="search"
+ placeholder="{{ trans('common.search') }}"
+ value="{{ $listOptions->getSearch() }}">
</form>
</div>
</div>
<div class="justify-flex-end">
- @include('common.sort', ['options' => [
- 'name' => trans('common.sort_name'),
- 'endpoint' => trans('settings.webhooks_endpoint'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- 'active' => trans('common.status'),
- ], 'order' => $listDetails['order'], 'sort' => $listDetails['sort'], 'type' => 'webhooks'])
+ @include('common.sort', $listOptions->getSortControlData())
</div>
</div>
@extends('layouts.tri')
@section('body')
- @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view])
+ @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view, 'listOptions' => $listOptions])
@stop
@section('right')
<div class="grid half v-center">
<h1 class="list-heading">{{ trans('entities.shelves') }}</h1>
<div class="text-right">
- @include('common.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'bookshelves'])
+ @include('common.sort', $listOptions->getSortControlData())
</div>
</div>
<h1 class="flex fit-content break-text">{{ $shelf->name }}</h1>
<div class="flex"></div>
<div class="flex fit-content text-m-right my-m ml-m">
- @include('common.sort', ['options' => [
- 'default' => trans('common.sort_default'),
- 'name' => trans('common.sort_name'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- ], 'order' => $order, 'sort' => $sort, 'type' => 'shelf_books'])
+ @include('common.sort', $listOptions->getSortControlData())
</div>
</div>
<div>
<div class="block inline mr-xs">
<form method="get" action="{{ url("/settings/users") }}">
- <input type="text" name="search" placeholder="{{ trans('settings.users_search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
+ <input type="text"
+ name="search"
+ placeholder="{{ trans('settings.users_search') }}"
+ value="{{ $listOptions->getSearch() }}">
</form>
</div>
</div>
<div class="justify-flex-end">
- @include('common.sort', ['options' => [
- 'name' => trans('common.sort_name'),
- 'email' => trans('auth.email'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- 'last_activity_at' => trans('settings.users_latest_activity'),
- ], 'order' => $listDetails['order'], 'sort' => $listDetails['sort'], 'type' => 'users'])
+ @include('common.sort', $listOptions->getSortControlData())
</div>
</div>