diff --git a/.github/workflows/analyse-php.yml b/.github/workflows/analyse-php.yml
index 191399d78..fd56a53ef 100644
--- a/.github/workflows/analyse-php.yml
+++ b/.github/workflows/analyse-php.yml
@@ -18,10 +18,10 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
- echo "::set-output name=dir::$(composer config cache-files-dir)"
+ echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-8.1
diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml
index e9b66a0a6..d762d7eab 100644
--- a/.github/workflows/test-migrations.yml
+++ b/.github/workflows/test-migrations.yml
@@ -21,10 +21,10 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
- echo "::set-output name=dir::$(composer config cache-files-dir)"
+ echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}
diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml
index 917038f59..4185e83c3 100644
--- a/.github/workflows/test-php.yml
+++ b/.github/workflows/test-php.yml
@@ -21,10 +21,10 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
- echo "::set-output name=dir::$(composer config cache-files-dir)"
+ echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}
diff --git a/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php b/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php
new file mode 100644
index 000000000..4958b6070
--- /dev/null
+++ b/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php
@@ -0,0 +1,30 @@
+select(['*'])
+ ->withCount(['trackedEvents'])
+ ->orderBy($listOptions->getSort(), $listOptions->getOrder());
+
+ if ($listOptions->getSearch()) {
+ $term = '%' . $listOptions->getSearch() . '%';
+ $query->where(function ($query) use ($term) {
+ $query->where('name', 'like', $term)
+ ->orWhere('endpoint', 'like', $term);
+ });
+ }
+
+ return $query->paginate($count);
+ }
+}
diff --git a/app/Actions/TagRepo.php b/app/Actions/TagRepo.php
index 2618ed2e9..cece30de0 100644
--- a/app/Actions/TagRepo.php
+++ b/app/Actions/TagRepo.php
@@ -4,6 +4,7 @@ namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@@ -20,8 +21,14 @@ class TagRepo
/**
* Start a query against all tags in the system.
*/
- public function queryWithTotals(string $searchTerm, string $nameFilter): Builder
+ public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilter): Builder
{
+ $searchTerm = $listOptions->getSearch();
+ $sort = $listOptions->getSort();
+ if ($sort === 'name' && $nameFilter) {
+ $sort = 'value';
+ }
+
$query = Tag::query()
->select([
'name',
@@ -32,7 +39,7 @@ class TagRepo
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
])
- ->orderBy($nameFilter ? 'value' : 'name');
+ ->orderBy($sort, $listOptions->getOrder());
if ($nameFilter) {
$query->where('name', '=', $nameFilter);
diff --git a/app/Auth/Queries/RolesAllPaginatedAndSorted.php b/app/Auth/Queries/RolesAllPaginatedAndSorted.php
new file mode 100644
index 000000000..9ee4f6c24
--- /dev/null
+++ b/app/Auth/Queries/RolesAllPaginatedAndSorted.php
@@ -0,0 +1,35 @@
+getSort();
+ if ($sort === 'created_at') {
+ $sort = 'users.created_at';
+ }
+
+ $query = Role::query()->select(['*'])
+ ->withCount(['users', 'permissions'])
+ ->orderBy($sort, $listOptions->getOrder());
+
+ if ($listOptions->getSearch()) {
+ $term = '%' . $listOptions->getSearch() . '%';
+ $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/Queries/AllUsersPaginatedAndSorted.php b/app/Auth/Queries/UsersAllPaginatedAndSorted.php
similarity index 63%
rename from app/Auth/Queries/AllUsersPaginatedAndSorted.php
rename to app/Auth/Queries/UsersAllPaginatedAndSorted.php
index 7b849eaf4..29b6a8969 100644
--- a/app/Auth/Queries/AllUsersPaginatedAndSorted.php
+++ b/app/Auth/Queries/UsersAllPaginatedAndSorted.php
@@ -3,6 +3,7 @@
namespace BookStack\Auth\Queries;
use BookStack\Auth\User;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
@@ -11,23 +12,23 @@ use Illuminate\Pagination\LengthAwarePaginator;
* user is assumed to be trusted. (Admin users).
* Email search can be abused to extract email addresses.
*/
-class AllUsersPaginatedAndSorted
+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';
+ }
$query = User::query()->select(['*'])
->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);
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/AuditLogController.php b/app/Http/Controllers/AuditLogController.php
index ec3f36975..da8009d78 100644
--- a/app/Http/Controllers/AuditLogController.php
+++ b/app/Http/Controllers/AuditLogController.php
@@ -3,6 +3,8 @@
namespace BookStack\Http\Controllers;
use BookStack\Actions\Activity;
+use BookStack\Actions\ActivityType;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -13,10 +15,15 @@ class AuditLogController extends Controller
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
- $listDetails = [
- 'order' => $request->get('order', 'desc'),
+ $sort = $request->get('sort', 'activity_date');
+ $order = $request->get('order', 'desc');
+ $listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([
+ 'created_at' => trans('settings.audit_table_date'),
+ 'type' => trans('settings.audit_table_event'),
+ ]);
+
+ $filters = [
'event' => $request->get('event', ''),
- 'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'user' => $request->get('user', ''),
@@ -25,39 +32,38 @@ class AuditLogController extends Controller
$query = Activity::query()
->with([
- 'entity' => function ($query) {
- $query->withTrashed();
- },
+ 'entity' => fn ($query) => $query->withTrashed(),
'user',
])
- ->orderBy($listDetails['sort'], $listDetails['order']);
+ ->orderBy($listOptions->getSort(), $listOptions->getOrder());
- if ($listDetails['event']) {
- $query->where('type', '=', $listDetails['event']);
+ if ($filters['event']) {
+ $query->where('type', '=', $filters['event']);
}
- if ($listDetails['user']) {
- $query->where('user_id', '=', $listDetails['user']);
+ if ($filters['user']) {
+ $query->where('user_id', '=', $filters['user']);
}
- if ($listDetails['date_from']) {
- $query->where('created_at', '>=', $listDetails['date_from']);
+ if ($filters['date_from']) {
+ $query->where('created_at', '>=', $filters['date_from']);
}
- if ($listDetails['date_to']) {
- $query->where('created_at', '<=', $listDetails['date_to']);
+ if ($filters['date_to']) {
+ $query->where('created_at', '<=', $filters['date_to']);
}
- if ($listDetails['ip']) {
- $query->where('ip', 'like', $listDetails['ip'] . '%');
+ if ($filters['ip']) {
+ $query->where('ip', 'like', $filters['ip'] . '%');
}
$activities = $query->paginate(100);
- $activities->appends($listDetails);
+ $activities->appends($request->all());
- $types = DB::table('activities')->select('type')->distinct()->pluck('type');
+ $types = ActivityType::all();
$this->setPageTitle(trans('settings.audit'));
return view('settings.audit', [
'activities' => $activities,
- 'listDetails' => $listDetails,
+ 'filters' => $filters,
+ 'listOptions' => $listOptions,
'activityTypes' => $types,
]);
}
diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php
index b323ae496..14c3af1cc 100644
--- a/app/Http/Controllers/BookController.php
+++ b/app/Http/Controllers/BookController.php
@@ -15,6 +15,7 @@ use BookStack\Exceptions\ImageUploadException;
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;
@@ -35,13 +36,16 @@ class BookController extends Controller
/**
* 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);
@@ -56,8 +60,7 @@ class BookController extends Controller
'popular' => $popular,
'new' => $new,
'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
+ 'listOptions' => $listOptions,
]);
}
diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php
index 3c63be631..537ea915b 100644
--- a/app/Http/Controllers/BookshelfController.php
+++ b/app/Http/Controllers/BookshelfController.php
@@ -10,6 +10,7 @@ use BookStack\Entities\Tools\ShelfContext;
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;
@@ -30,18 +31,16 @@ class BookshelfController extends Controller
/**
* 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);
@@ -55,9 +54,7 @@ class BookshelfController extends Controller
'popular' => $popular,
'new' => $new,
'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
- 'sortOptions' => $sortOptions,
+ 'listOptions' => $listOptions,
]);
}
@@ -100,16 +97,21 @@ class BookshelfController extends Controller
*
* @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();
@@ -124,8 +126,7 @@ class BookshelfController extends Controller
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
'view' => $view,
'activity' => $activities->entityActivity($shelf, 20, 1),
- 'order' => $order,
- 'sort' => $sort,
+ 'listOptions' => $listOptions,
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
]);
}
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index f38bd71df..c3c8d1066 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -10,13 +10,15 @@ use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PageContent;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Http\Request;
class HomeController extends Controller
{
/**
* Display the homepage.
*/
- public function index(ActivityQueries $activities)
+ public function index(Request $request, ActivityQueries $activities)
{
$activity = $activities->latest(10);
$draftPages = [];
@@ -61,33 +63,27 @@ class HomeController extends Controller
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
$view = setting()->getForCurrentUser($key . '_view_type');
- $sort = setting()->getForCurrentUser($key . '_sort', 'name');
- $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
-
- $sortOptions = [
- 'name' => trans('common.sort_name'),
+ $listOptions = SimpleListOptions::fromRequest($request, $key)->withSortOptions([
+ 'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
- ];
+ ]);
$commonData = array_merge($commonData, [
'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
- 'sortOptions' => $sortOptions,
+ 'listOptions' => $listOptions,
]);
}
if ($homepageOption === 'bookshelves') {
- $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
+ $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('home.shelves', $data);
}
if ($homepageOption === 'books') {
- $bookRepo = app(BookRepo::class);
- $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
+ $books = app(BookRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
$data = array_merge($commonData, ['books' => $books]);
return view('home.books', $data);
diff --git a/app/Http/Controllers/PageRevisionController.php b/app/Http/Controllers/PageRevisionController.php
index 85ee6c2bc..3da5e7c2d 100644
--- a/app/Http/Controllers/PageRevisionController.php
+++ b/app/Http/Controllers/PageRevisionController.php
@@ -8,6 +8,8 @@ use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Http\Request;
use Ssddanbrown\HtmlDiff\Diff;
class PageRevisionController extends Controller
@@ -24,22 +26,29 @@ class PageRevisionController extends Controller
*
* @throws NotFoundException
*/
- public function index(string $bookSlug, string $pageSlug)
+ public function index(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
+ 'id' => trans('entities.pages_revisions_sort_number')
+ ]);
+
$revisions = $page->revisions()->select([
- 'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
- 'type', 'revision_number', 'summary',
- ])
+ 'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
+ 'type', 'revision_number', 'summary',
+ ])
->selectRaw("IF(markdown = '', false, true) as is_markdown")
->with(['page.book', 'createdBy'])
- ->get();
+ ->reorder('id', $listOptions->getOrder())
+ ->reorder('created_at', $listOptions->getOrder())
+ ->paginate(50);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
return view('pages.revisions', [
- 'revisions' => $revisions,
- 'page' => $page,
+ 'revisions' => $revisions,
+ 'page' => $page,
+ 'listOptions' => $listOptions,
]);
}
diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php
index fee31ffbf..a9be19e0c 100644
--- a/app/Http/Controllers/RoleController.php
+++ b/app/Http/Controllers/RoleController.php
@@ -3,19 +3,18 @@
namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionsRepo;
+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;
class RoleController extends Controller
{
- protected $permissionsRepo;
+ protected PermissionsRepo $permissionsRepo;
- /**
- * PermissionController constructor.
- */
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
@@ -24,14 +23,27 @@ 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();
+
+ $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, $listOptions);
+ $roles->appends($listOptions->getPaginationAppends());
$this->setPageTitle(trans('settings.roles'));
- return view('settings.roles.index', ['roles' => $roles]);
+ return view('settings.roles.index', [
+ 'roles' => $roles,
+ 'listOptions' => $listOptions,
+ ]);
}
/**
@@ -75,16 +87,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/TagController.php b/app/Http/Controllers/TagController.php
index 056cc9902..6c2876043 100644
--- a/app/Http/Controllers/TagController.php
+++ b/app/Http/Controllers/TagController.php
@@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Actions\TagRepo;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
class TagController extends Controller
@@ -19,22 +20,25 @@ class TagController extends Controller
*/
public function index(Request $request)
{
- $search = $request->get('search', '');
+ $listOptions = SimpleListOptions::fromRequest($request, 'tags')->withSortOptions([
+ 'name' => trans('common.sort_name'),
+ 'usages' => trans('entities.tags_usages'),
+ ]);
+
$nameFilter = $request->get('name', '');
$tags = $this->tagRepo
- ->queryWithTotals($search, $nameFilter)
+ ->queryWithTotals($listOptions, $nameFilter)
->paginate(50)
- ->appends(array_filter([
- 'search' => $search,
+ ->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [
'name' => $nameFilter,
- ]));
+ ])));
$this->setPageTitle(trans('entities.tags'));
return view('tags.index', [
- 'tags' => $tags,
- 'search' => $search,
- 'nameFilter' => $nameFilter,
+ 'tags' => $tags,
+ 'nameFilter' => $nameFilter,
+ 'listOptions' => $listOptions,
]);
}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 895481d02..f69f00cf7 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -3,13 +3,13 @@
namespace BookStack\Http\Controllers;
use BookStack\Auth\Access\SocialAuthService;
-use BookStack\Auth\Queries\AllUsersPaginatedAndSorted;
+use BookStack\Auth\Queries\UsersAllPaginatedAndSorted;
use BookStack\Auth\Role;
-use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
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;
@@ -21,9 +21,6 @@ class UserController extends Controller
protected UserRepo $userRepo;
protected ImageRepo $imageRepo;
- /**
- * UserController constructor.
- */
public function __construct(UserRepo $userRepo, ImageRepo $imageRepo)
{
$this->userRepo = $userRepo;
@@ -36,20 +33,23 @@ class UserController extends Controller
public function index(Request $request)
{
$this->checkPermission('users-manage');
- $listDetails = [
- 'order' => $request->get('order', 'asc'),
- 'search' => $request->get('search', ''),
- 'sort' => $request->get('sort', 'name'),
- ];
- $users = (new AllUsersPaginatedAndSorted())->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($listDetails);
+ $users->appends($listOptions->getPaginationAppends());
return view('users.index', [
'users' => $users,
- 'listDetails' => $listDetails,
+ 'listOptions' => $listOptions,
]);
}
@@ -107,9 +107,8 @@ class UserController extends Controller
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
- /** @var User $user */
- $user = User::query()->with(['apiTokens', 'mfaValues'])->findOrFail($id);
-
+ $user = $this->userRepo->getById($id);
+ $user->load(['apiTokens', 'mfaValues']);
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
@@ -202,137 +201,4 @@ class UserController extends Controller
return redirect('/settings/users');
}
-
- /**
- * Update the user's preferred book-list display setting.
- */
- public function switchBooksView(Request $request, int $id)
- {
- return $this->switchViewType($id, $request, 'books');
- }
-
- /**
- * Update the user's preferred shelf-list display setting.
- */
- public function switchShelvesView(Request $request, int $id)
- {
- return $this->switchViewType($id, $request, 'bookshelves');
- }
-
- /**
- * Update the user's preferred shelf-view book list display setting.
- */
- public function switchShelfView(Request $request, int $id)
- {
- return $this->switchViewType($id, $request, 'bookshelf');
- }
-
- /**
- * For a type of list, switch with stored view type for a user.
- */
- protected function switchViewType(int $userId, Request $request, string $listName)
- {
- $this->checkPermissionOrCurrentUser('users-manage', $userId);
-
- $viewType = $request->get('view_type');
- if (!in_array($viewType, ['grid', 'list'])) {
- $viewType = 'list';
- }
-
- $user = $this->userRepo->getById($userId);
- $key = $listName . '_view_type';
- setting()->putUser($user, $key, $viewType);
-
- return redirect()->back(302, [], "/settings/users/$userId");
- }
-
- /**
- * Change the stored sort type for a particular view.
- */
- public function changeSort(Request $request, string $id, string $type)
- {
- $validSortTypes = ['books', 'bookshelves', 'shelf_books'];
- if (!in_array($type, $validSortTypes)) {
- return redirect()->back(500);
- }
-
- return $this->changeListSort($id, $request, $type);
- }
-
- /**
- * Toggle dark mode for the current user.
- */
- public function toggleDarkMode()
- {
- $enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
- setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true');
-
- return redirect()->back();
- }
-
- /**
- * Update the stored section expansion preference for the given user.
- */
- public function updateExpansionPreference(Request $request, string $id, string $key)
- {
- $this->checkPermissionOrCurrentUser('users-manage', $id);
- $keyWhitelist = ['home-details'];
- if (!in_array($key, $keyWhitelist)) {
- return response('Invalid key', 500);
- }
-
- $newState = $request->get('expand', 'false');
-
- $user = $this->userRepo->getById($id);
- setting()->putUser($user, 'section_expansion#' . $key, $newState);
-
- return response('', 204);
- }
-
- public function updateCodeLanguageFavourite(Request $request)
- {
- $validated = $this->validate($request, [
- 'language' => ['required', 'string', 'max:20'],
- 'active' => ['required', 'bool'],
- ]);
-
- $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
- $currentFavorites = array_filter(explode(',', $currentFavoritesStr));
-
- $isFav = in_array($validated['language'], $currentFavorites);
- if (!$isFav && $validated['active']) {
- $currentFavorites[] = $validated['language'];
- } elseif ($isFav && !$validated['active']) {
- $index = array_search($validated['language'], $currentFavorites);
- array_splice($currentFavorites, $index, 1);
- }
-
- 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');
- if (!in_array($sort, ['name', 'created_at', 'updated_at', 'default'])) {
- $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");
- }
}
diff --git a/app/Http/Controllers/UserPreferencesController.php b/app/Http/Controllers/UserPreferencesController.php
new file mode 100644
index 000000000..972742e03
--- /dev/null
+++ b/app/Http/Controllers/UserPreferencesController.php
@@ -0,0 +1,134 @@
+userRepo = $userRepo;
+ }
+
+ /**
+ * Update the user's preferred book-list display setting.
+ */
+ public function switchBooksView(Request $request, int $id)
+ {
+ return $this->switchViewType($id, $request, 'books');
+ }
+
+ /**
+ * Update the user's preferred shelf-list display setting.
+ */
+ public function switchShelvesView(Request $request, int $id)
+ {
+ return $this->switchViewType($id, $request, 'bookshelves');
+ }
+
+ /**
+ * Update the user's preferred shelf-view book list display setting.
+ */
+ public function switchShelfView(Request $request, int $id)
+ {
+ return $this->switchViewType($id, $request, 'bookshelf');
+ }
+
+ /**
+ * For a type of list, switch with stored view type for a user.
+ */
+ protected function switchViewType(int $userId, Request $request, string $listName)
+ {
+ $this->checkPermissionOrCurrentUser('users-manage', $userId);
+
+ $viewType = $request->get('view_type');
+ if (!in_array($viewType, ['grid', 'list'])) {
+ $viewType = 'list';
+ }
+
+ $user = $this->userRepo->getById($userId);
+ $key = $listName . '_view_type';
+ setting()->putUser($user, $key, $viewType);
+
+ return redirect()->back(302, [], "/settings/users/$userId");
+ }
+
+ /**
+ * Change the stored sort type for a particular view.
+ */
+ public function changeSort(Request $request, string $id, string $type)
+ {
+ $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags', 'page_revisions'];
+ if (!in_array($type, $validSortTypes)) {
+ return redirect()->back(500);
+ }
+
+ $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}");
+ }
+
+ /**
+ * Toggle dark mode for the current user.
+ */
+ public function toggleDarkMode()
+ {
+ $enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
+ setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true');
+
+ return redirect()->back();
+ }
+
+ /**
+ * Update the stored section expansion preference for the given user.
+ */
+ public function updateExpansionPreference(Request $request, string $id, string $key)
+ {
+ $this->checkPermissionOrCurrentUser('users-manage', $id);
+ $keyWhitelist = ['home-details'];
+ if (!in_array($key, $keyWhitelist)) {
+ return response('Invalid key', 500);
+ }
+
+ $newState = $request->get('expand', 'false');
+
+ $user = $this->userRepo->getById($id);
+ setting()->putUser($user, 'section_expansion#' . $key, $newState);
+
+ return response('', 204);
+ }
+
+ public function updateCodeLanguageFavourite(Request $request)
+ {
+ $validated = $this->validate($request, [
+ 'language' => ['required', 'string', 'max:20'],
+ 'active' => ['required', 'bool'],
+ ]);
+
+ $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
+ $currentFavorites = array_filter(explode(',', $currentFavoritesStr));
+
+ $isFav = in_array($validated['language'], $currentFavorites);
+ if (!$isFav && $validated['active']) {
+ $currentFavorites[] = $validated['language'];
+ } elseif ($isFav && !$validated['active']) {
+ $index = array_search($validated['language'], $currentFavorites);
+ array_splice($currentFavorites, $index, 1);
+ }
+
+ setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
+ }
+}
diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php
index 264921dfc..c72dcc510 100644
--- a/app/Http/Controllers/WebhookController.php
+++ b/app/Http/Controllers/WebhookController.php
@@ -3,7 +3,9 @@
namespace BookStack\Http\Controllers;
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
@@ -18,16 +20,25 @@ class WebhookController extends Controller
/**
* Show all webhooks configured in the system.
*/
- public function index()
+ public function index(Request $request)
{
- $webhooks = Webhook::query()
- ->orderBy('name', 'desc')
- ->with('trackedEvents')
- ->get();
+ $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, $listOptions);
+ $webhooks->appends($listOptions->getPaginationAppends());
$this->setPageTitle(trans('settings.webhooks'));
- return view('settings.webhooks.index', ['webhooks' => $webhooks]);
+ return view('settings.webhooks.index', [
+ 'webhooks' => $webhooks,
+ 'listOptions' => $listOptions,
+ ]);
}
/**
diff --git a/app/Util/SimpleListOptions.php b/app/Util/SimpleListOptions.php
new file mode 100644
index 000000000..81d8a5876
--- /dev/null
+++ b/app/Util/SimpleListOptions.php
@@ -0,0 +1,104 @@
+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, bool $sortDescDefault = false): self
+ {
+ $search = $request->get('search', '');
+ $sort = setting()->getForCurrentUser($typeKey . '_sort', '');
+ $order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');
+
+ return new self($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,
+ ];
+ }
+}
diff --git a/resources/js/components/entity-permissions.js b/resources/js/components/entity-permissions.js
index c67c85f19..0dec5ca09 100644
--- a/resources/js/components/entity-permissions.js
+++ b/resources/js/components/entity-permissions.js
@@ -62,7 +62,7 @@ class EntityPermissions {
}
removeRowOnButtonClick(button) {
- const row = button.closest('.content-permissions-row');
+ const row = button.closest('.item-list-row');
const roleId = button.dataset.roleId;
const roleName = button.dataset.roleName;
diff --git a/resources/js/components/list-sort-control.js b/resources/js/components/list-sort-control.js
index 23fc64ae6..3b642dbde 100644
--- a/resources/js/components/list-sort-control.js
+++ b/resources/js/components/list-sort-control.js
@@ -1,17 +1,22 @@
/**
* ListSortControl
* Manages the logic for the control which provides list sorting options.
+ * @extends {Component}
*/
class ListSortControl {
- constructor(elem) {
- this.elem = elem;
- this.menu = elem.querySelector('ul');
+ setup() {
+ this.elem = this.$el;
+ this.menu = this.$refs.menu;
- this.sortInput = elem.querySelector('[name="sort"]');
- this.orderInput = elem.querySelector('[name="order"]');
- this.form = elem.querySelector('form');
+ this.sortInput = this.$refs.sort;
+ this.orderInput = this.$refs.order;
+ this.form = this.$refs.form;
+ this.setupListeners();
+ }
+
+ setupListeners() {
this.menu.addEventListener('click', event => {
if (event.target.closest('[data-sort-value]') !== null) {
this.sortOptionClick(event);
@@ -34,8 +39,7 @@ class ListSortControl {
sortDirectionClick(event) {
const currentDir = this.orderInput.value;
- const newDir = (currentDir === 'asc') ? 'desc' : 'asc';
- this.orderInput.value = newDir;
+ this.orderInput.value = (currentDir === 'asc') ? 'desc' : 'asc';
event.preventDefault();
this.form.submit();
}
diff --git a/resources/js/components/permissions-table.js b/resources/js/components/permissions-table.js
index df3c055ca..d33c9928f 100644
--- a/resources/js/components/permissions-table.js
+++ b/resources/js/components/permissions-table.js
@@ -3,6 +3,8 @@ class PermissionsTable {
setup() {
this.container = this.$el;
+ this.cellSelector = this.$opts.cellSelector || 'td,th';
+ this.rowSelector = this.$opts.rowSelector || 'tr';
// Handle toggle all event
for (const toggleAllElem of (this.$manyRefs.toggleAll || [])) {
@@ -27,15 +29,15 @@ class PermissionsTable {
toggleRowClick(event) {
event.preventDefault();
- this.toggleAllInElement(event.target.closest('tr'));
+ this.toggleAllInElement(event.target.closest(this.rowSelector));
}
toggleColumnClick(event) {
event.preventDefault();
- const tableCell = event.target.closest('th,td');
+ const tableCell = event.target.closest(this.cellSelector);
const colIndex = Array.from(tableCell.parentElement.children).indexOf(tableCell);
- const tableRows = tableCell.closest('table').querySelectorAll('tr');
+ const tableRows = this.container.querySelectorAll(this.rowSelector);
const inputsToToggle = [];
for (let row of tableRows) {
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index bf6201900..e7fbe37d9 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -233,12 +233,14 @@ return [
'pages_permissions_success' => 'Page permissions updated',
'pages_revision' => 'Revision',
'pages_revisions' => 'Page Revisions',
+ 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
'pages_revisions_named' => 'Page Revisions for :pageName',
'pages_revision_named' => 'Page Revision for :pageName',
'pages_revision_restored_from' => 'Restored from #:id; :summary',
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'pages_revisions_number' => '#',
+ 'pages_revisions_sort_number' => 'Revision Number',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +277,7 @@ return [
'shelf_tags' => 'Shelf Tags',
'tag' => 'Tag',
'tags' => 'Tags',
+ 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
'tag_name' => 'Tag Name',
'tag_value' => 'Tag Value (Optional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php
index 1ad271e7c..f4204dd68 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',
@@ -172,6 +177,7 @@ return [
// Users
'users' => 'Users',
+ 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
@@ -241,6 +247,8 @@ return [
// Webhooks
'webhooks' => 'Webhooks',
+ 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+ 'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
'webhooks_create' => 'Create New Webhook',
'webhooks_none_created' => 'No webhooks have yet been created.',
'webhooks_edit' => 'Edit Webhook',
diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss
index 0398224ca..6058add82 100644
--- a/resources/sass/_blocks.scss
+++ b/resources/sass/_blocks.scss
@@ -286,35 +286,10 @@
margin-bottom: 0;
}
-td .tag-item {
+.item-list-row .tag-item {
margin-bottom: 0;
}
-/**
- * Pill boxes
- */
-
-.pill {
- display: inline-block;
- border: 1px solid currentColor;
- padding: .2em .8em;
- font-size: 0.8em;
- border-radius: 1rem;
- position: relative;
- overflow: hidden;
- line-height: 1.4;
- &:before {
- content: '';
- background-color: currentColor;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- opacity: 0.1;
- }
-}
-
/**
* API Docs
*/
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 9fdd5a611..acb45100f 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -798,37 +798,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
max-width: 500px;
}
-.content-permissions {
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
-}
-.content-permissions-row {
- border: 1.5px solid;
- @include lightDark(border-color, #E2E2E2, #444);
- border-bottom-width: 0;
- label {
- padding-bottom: 0;
- }
- &:hover {
- @include lightDark(background-color, #F2F2F2, #333);
- }
-}
-.content-permissions-row:first-child {
- border-radius: 4px 4px 0 0;
-}
-.content-permissions-row:last-child {
- border-radius: 0 0 4px 4px;
- border-bottom-width: 1.5px;
-}
-.content-permissions-row:first-child:last-child {
- border-radius: 4px;
-}
-.content-permissions-row-toggle-all {
- visibility: hidden;
-}
-.content-permissions-row:hover .content-permissions-row-toggle-all {
- visibility: visible;
-}
-
.template-item {
cursor: pointer;
position: relative;
@@ -969,4 +938,48 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.dropdown-search-dropdown .dropdown-search-list {
max-height: 240px;
}
+}
+
+.item-list {
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+}
+.item-list-row {
+ border: 1.5px solid;
+ @include lightDark(border-color, #E2E2E2, #444);
+ border-bottom-width: 0;
+ label {
+ padding-bottom: 0;
+ }
+ &:hover {
+ @include lightDark(background-color, #F6F6F6, #333);
+ }
+}
+.item-list-row:first-child {
+ border-radius: 4px 4px 0 0;
+}
+.item-list-row:last-child {
+ border-radius: 0 0 4px 4px;
+ border-bottom-width: 1.5px;
+}
+.item-list-row:first-child:last-child {
+ border-radius: 4px;
+}
+.item-list-row-toggle-all {
+ visibility: hidden;
+}
+.item-list-row:hover .item-list-row-toggle-all {
+ visibility: visible;
+}
+
+.status-indicator-active, .status-indicator-inactive {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ display: inline-block;
+}
+.status-indicator-active {
+ background-color: $positive;
+}
+.status-indicator-inactive {
+ background-color: $negative;
}
\ No newline at end of file
diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss
index cfb8397c9..a5f895f80 100644
--- a/resources/sass/_layout.scss
+++ b/resources/sass/_layout.scss
@@ -144,6 +144,10 @@ body.flexbox {
flex-direction: column;
}
+.flex-container-row.inline, .flex-container-column.inline {
+ display: inline-flex !important;
+}
+
.flex-container-column.wrap, .flex-container-row.wrap {
flex-wrap: wrap;
}
@@ -156,6 +160,23 @@ body.flexbox {
flex-basis: auto;
flex-grow: 0;
}
+ &.fill-area {
+ flex-grow: 1;
+ flex-shrink: 0;
+ min-width: fit-content;
+ }
+}
+
+.flex-2 {
+ min-height: 0;
+ flex: 2;
+ max-width: 100%;
+}
+
+.flex-3 {
+ min-height: 0;
+ flex: 3;
+ max-width: 100%;
}
.flex-none {
@@ -178,6 +199,36 @@ body.flexbox {
align-items: center;
}
+/**
+ * Min width utilities
+ */
+.min-width-xxxxs {
+ min-width: 60px;
+}
+.min-width-xxxs {
+ min-width: 80px;
+}
+.min-width-xxs {
+ min-width: 100px;
+}
+.min-width-xs {
+ min-width: 120px;
+}
+.min-width-s {
+ min-width: 160px;
+}
+.min-width-m {
+ min-width: 200px;
+}
+.min-width-l {
+ min-width: 240px;
+}
+.min-width-xl {
+ min-width: 280px;
+}
+.min-width-xxl {
+ min-width: 320px;
+}
/**
* Display and float utilities
diff --git a/resources/sass/_opacity.scss b/resources/sass/_opacity.scss
new file mode 100644
index 000000000..235aed48e
--- /dev/null
+++ b/resources/sass/_opacity.scss
@@ -0,0 +1,28 @@
+
+.opacity-10 {
+ opacity: 0.1;
+}
+.opacity-20 {
+ opacity: 0.2;
+}
+.opacity-30 {
+ opacity: 0.3;
+}
+.opacity-40 {
+ opacity: 0.4;
+}
+.opacity-50 {
+ opacity: 0.5;
+}
+.opacity-60 {
+ opacity: 0.6;
+}
+.opacity-70 {
+ opacity: 0.7;
+}
+.opacity-80 {
+ opacity: 0.8;
+}
+.opacity-90 {
+ opacity: 0.9;
+}
\ No newline at end of file
diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss
index ab97466a5..5e31dbdfb 100644
--- a/resources/sass/styles.scss
+++ b/resources/sass/styles.scss
@@ -4,6 +4,7 @@
@import "variables";
@import "mixins";
@import "spacing";
+@import "opacity";
@import "html";
@import "text";
@import "colors";
@@ -352,15 +353,4 @@ input.scroll-box-search, .scroll-box-header-item {
transform: rotate(180deg);
}
}
-}
-
-table.table .table-user-item {
- display: grid;
- grid-template-columns: 42px 1fr;
- align-items: center;
-}
-table.table .table-entity-item {
- display: grid;
- grid-template-columns: 36px 1fr;
- align-items: center;
}
\ No newline at end of file
diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php
index 6573bbe6a..447d6fd44 100644
--- a/resources/views/books/index.blade.php
+++ b/resources/views/books/index.blade.php
@@ -1,7 +1,7 @@
@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')
diff --git a/resources/views/books/parts/list.blade.php b/resources/views/books/parts/list.blade.php
index 30b076613..2cf83dfa9 100644
--- a/resources/views/books/parts/list.blade.php
+++ b/resources/views/books/parts/list.blade.php
@@ -2,13 +2,7 @@
{{ trans('entities.books') }}
-
- @include('entities.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())
@if(count($books) > 0)
@@ -19,11 +13,11 @@
@endforeach
@else
-
+
@foreach($books as $key => $book)
@include('entities.grid-item', ['entity' => $book])
@endforeach
-
+
@endif
{!! $books->render() !!}
diff --git a/resources/views/entities/sort.blade.php b/resources/views/common/sort.blade.php
similarity index 52%
rename from resources/views/entities/sort.blade.php
rename to resources/views/common/sort.blade.php
index f81ed797f..996f7a837 100644
--- a/resources/views/entities/sort.blade.php
+++ b/resources/views/common/sort.blade.php
@@ -2,25 +2,40 @@
$selectedSort = (isset($sort) && array_key_exists($sort, $options)) ? $sort : array_keys($options)[0];
$order = (isset($order) && in_array($order, ['asc', 'desc'])) ? $order : 'asc';
?>
-
+
{{ trans('common.sort') }}
-
-
+
- {{ $activities->links() }}
+
+
{{ $activities->links() }}
+
+ @include('common.sort', array_merge($listOptions->getSortControlData(), ['useQuery' => true]))
+
+
-
-
-
- {{ trans('settings.audit_table_user') }}
-
- {{ trans('settings.audit_table_event') }}
-
- {{ trans('settings.audit_table_related') }}
- {{ trans('settings.audit_table_ip') }}
-
- {{ trans('settings.audit_table_date') }}
-
+
+
+
{{ trans('settings.audit_table_user') }}
+
{{ trans('settings.audit_table_event') }}
+
{{ trans('settings.audit_table_related') }}
+
+
{{ trans('settings.audit_table_ip') }}
+
{{ trans('settings.audit_table_date') }}
+
+
@foreach($activities as $activity)
-
-
+
+
@include('settings.parts.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
-
- {{ $activity->type }}
-
+
+ {{ trans('settings.audit_table_event') }}: {{ $activity->type }}
+
- {{ $activity->ip }}
- {{ $activity->created_at }}
-
+
+
+
{{ trans('settings.audit_table_ip') }}: {{ $activity->ip }}
+
{{ trans('settings.audit_table_date') }}: {{ $activity->created_at }}
+
+
@endforeach
-
-
+
- {{ $activities->links() }}
+
+ {{ $activities->links() }}
+
diff --git a/resources/views/settings/parts/table-user.blade.php b/resources/views/settings/parts/table-user.blade.php
index a8f2777f0..d29ad1979 100644
--- a/resources/views/settings/parts/table-user.blade.php
+++ b/resources/views/settings/parts/table-user.blade.php
@@ -3,9 +3,9 @@ $user - User mode to display, Can be null.
$user_id - Id of user to show. Must be provided.
--}}
@if($user)
-
-
- {{ $user->name }}
+
+
+ {{ $user->name }}
@else
[ID: {{ $user_id }}] {{ trans('common.deleted_user') }}
diff --git a/resources/views/settings/recycle-bin/index.blade.php b/resources/views/settings/recycle-bin/index.blade.php
index 56e2437fe..9e82ba467 100644
--- a/resources/views/settings/recycle-bin/index.blade.php
+++ b/resources/views/settings/recycle-bin/index.blade.php
@@ -8,11 +8,11 @@
{{ trans('settings.recycle_bin') }}
-
-
-
{{ trans('settings.recycle_bin_desc') }}
+
+
+
{{ trans('settings.recycle_bin_desc') }}
-
+
-
- {!! $deletions->links() !!}
+
+ {!! $deletions->links() !!}
+
-
-
- {{ trans('settings.recycle_bin_deleted_item') }}
- {{ trans('settings.recycle_bin_deleted_parent') }}
- {{ trans('settings.recycle_bin_deleted_by') }}
- {{ trans('settings.recycle_bin_deleted_at') }}
-
-
+
+
+
{{ trans('settings.audit_deleted_item') }}
+
{{ trans('settings.recycle_bin_deleted_parent') }}
+
{{ trans('settings.recycle_bin_deleted_by') }}
+
{{ trans('settings.recycle_bin_deleted_at') }}
+
+
@if(count($deletions) === 0)
-
-
- {{ trans('settings.recycle_bin_contents_empty') }}
-
-
+
+
{{ trans('settings.recycle_bin_contents_empty') }}
+
@endif
@foreach($deletions as $deletion)
-
-
-
-
@icon($deletion->deletable->getType())
-
- {{ $deletion->deletable->name }}
-
-
- @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
-
- @endif
- @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
-
-
- @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
-
-
- @endif
- @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
-
-
- @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
-
-
- @endif
-
-
- @if($deletion->deletable->getParent())
-
-
@icon($deletion->deletable->getParent()->getType())
-
- {{ $deletion->deletable->getParent()->name }}
-
-
- @endif
-
- @include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])
- {{ $deletion->created_at }}
-
-
- {{ trans('common.actions') }}
-
-
-
-
+ @include('settings.recycle-bin.parts.recycle-bin-list-item', ['deletion' => $deletion])
@endforeach
-
+
- {!! $deletions->links() !!}
+
+ {!! $deletions->links() !!}
+
diff --git a/resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php b/resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php
new file mode 100644
index 000000000..8af598b1e
--- /dev/null
+++ b/resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php
@@ -0,0 +1,48 @@
+
+
+
+
@icon($deletion->deletable->getType())
+
+ {{ $deletion->deletable->name }}
+
+
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
+
+
+ @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
+
+
+ @endif
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+
+
+ @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
+
+
+ @endif
+
+
+ @if($deletion->deletable->getParent())
+
{{ trans('settings.recycle_bin_deleted_parent') }}:
+
+
@icon($deletion->deletable->getParent()->getType())
+
+ {{ $deletion->deletable->getParent()->name }}
+
+
+ @endif
+
+
+
{{ trans('settings.recycle_bin_deleted_by') }}: @include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])
+
+
{{ trans('settings.recycle_bin_deleted_at') }}: {{ $deletion->created_at }}
+
+
+ {{ trans('common.actions') }}
+
+
+
+
\ No newline at end of file
diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php
index 4c3b5625a..27ee9ce3f 100644
--- a/resources/views/settings/roles/index.blade.php
+++ b/resources/views/settings/roles/index.blade.php
@@ -12,30 +12,37 @@
{{ trans('settings.role_user_roles') }}
-
-
- {{ trans('settings.role_name') }}
-
- {{ trans('settings.users') }}
-
- @foreach($roles as $role)
-
- id}") }}">{{ $role->display_name }}
-
- @if($role->mfa_enforced)
- @icon('lock')
- @endif
- {{ $role->description }}
-
- {{ $role->users->count() }}
-
- @endforeach
-
+
{{ trans('settings.roles_index_desc') }}
+
+
+
+ @include('common.sort', $listOptions->getSortControlData())
+
+
+
+
+ @foreach($roles as $role)
+ @include('settings.roles.parts.roles-list-item', ['role' => $role])
+ @endforeach
+
+
+
+ {{ $roles->links() }}
+
diff --git a/resources/views/settings/roles/parts/asset-permissions-row.blade.php b/resources/views/settings/roles/parts/asset-permissions-row.blade.php
new file mode 100644
index 000000000..df179a985
--- /dev/null
+++ b/resources/views/settings/roles/parts/asset-permissions-row.blade.php
@@ -0,0 +1,32 @@
+
+
+
+ {{ trans('common.create') }}
+ @if($permissionPrefix === 'page' || $permissionPrefix === 'chapter')
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-own', 'label' => trans('settings.role_own')])
+
+ @endif
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => trans('settings.role_all')])
+
+
+ {{ trans('common.view') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-own', 'label' => trans('settings.role_own')])
+
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-all', 'label' => trans('settings.role_all')])
+
+
+ {{ trans('common.edit') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])
+
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])
+
+
+ {{ trans('common.delete') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])
+
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])
+
+
\ No newline at end of file
diff --git a/resources/views/settings/roles/parts/form.blade.php b/resources/views/settings/roles/parts/form.blade.php
index 044b4ceb4..8534b7fdb 100644
--- a/resources/views/settings/roles/parts/form.blade.php
+++ b/resources/views/settings/roles/parts/form.blade.php
@@ -56,174 +56,30 @@
{{ trans('settings.role_asset_admins') }}
@endif
-
-
-
+
- {{ trans('common.create') }}
- {{ trans('common.view') }}
- {{ trans('common.edit') }}
- {{ trans('common.delete') }}
-
-
-
- {{ trans('entities.shelves') }}
- {{ trans('common.toggle_all') }}
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
-
-
-
-
- {{ trans('entities.books') }}
- {{ trans('common.toggle_all') }}
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
-
-
-
-
- {{ trans('entities.chapters') }}
- {{ trans('common.toggle_all') }}
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
-
-
-
-
- {{ trans('entities.pages') }}
- {{ trans('common.toggle_all') }}
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
-
-
-
-
- {{ trans('entities.images') }}
- {{ trans('common.toggle_all') }}
-
- @include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])
- {{ trans('settings.role_controlled_by_asset') }}1
-
- @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
-
-
-
-
- {{ trans('entities.attachments') }}
- {{ trans('common.toggle_all') }}
-
- @include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])
- {{ trans('settings.role_controlled_by_asset') }}
-
- @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
-
-
-
-
- {{ trans('entities.comments') }}
- {{ trans('common.toggle_all') }}
-
- @include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])
- {{ trans('settings.role_controlled_by_asset') }}
-
- @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
-
-
- @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
-
- @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
-
-
-
+
+ {{ trans('common.create') }}
+ {{ trans('common.view') }}
+ {{ trans('common.edit') }}
+ {{ trans('common.delete') }}
+
+ @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.shelves'), 'permissionPrefix' => 'bookshelf'])
+ @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.books'), 'permissionPrefix' => 'book'])
+ @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.chapters'), 'permissionPrefix' => 'chapter'])
+ @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.pages'), 'permissionPrefix' => 'page'])
+ @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image', 'refMark' => '1'])
+ @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.attachments'), 'permissionPrefix' => 'attachment'])
+ @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.comments'), 'permissionPrefix' => 'comment'])
+
-
+
1 {{ trans('settings.role_asset_image_view_note') }}
diff --git a/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php b/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php
new file mode 100644
index 000000000..1ec3d2257
--- /dev/null
+++ b/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php
@@ -0,0 +1,26 @@
+
+
+
+ {{ trans('common.create') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => ''])
+
+
+ {{ trans('common.view') }}
+ {{ trans('settings.role_controlled_by_asset') }}@if($refMark ?? false){{ $refMark }} @endif
+
+
+ {{ trans('common.edit') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])
+
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])
+
+
+ {{ trans('common.delete') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])
+
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])
+
+
\ No newline at end of file
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 @@
+
+
+
+ {{ trans_choice('settings.roles_x_users_assigned', $role->users_count, ['count' => $role->users_count]) }}
+
+ {{ trans_choice('settings.roles_x_permissions_provided', $role->permissions_count, ['count' => $role->permissions_count]) }}
+
+
\ No newline at end of file
diff --git a/resources/views/settings/webhooks/index.blade.php b/resources/views/settings/webhooks/index.blade.php
index bbe58453f..a564effe2 100644
--- a/resources/views/settings/webhooks/index.blade.php
+++ b/resources/views/settings/webhooks/index.blade.php
@@ -8,48 +8,48 @@
-
+
{{ trans('settings.webhooks') }}
-
- @if(count($webhooks) > 0)
+
{{ trans('settings.webhooks_index_desc') }}
-
-
- {{ trans('common.name') }}
- {{ trans('settings.webhook_events_table_header') }}
- {{ trans('common.status') }}
-
+
+
+
+ @include('common.sort', $listOptions->getSortControlData())
+
+
+
+ @if(count($webhooks) > 0)
+
@foreach($webhooks as $webhook)
-
-
- {{ $webhook->name }}
- {{ $webhook->endpoint }}
-
-
- @if($webhook->tracksEvent('all'))
- {{ trans('settings.webhooks_events_all') }}
- @else
- {{ $webhook->trackedEvents->count() }}
- @endif
-
-
- {{ trans('common.status_' . ($webhook->active ? 'active' : 'inactive')) }}
-
-
+ @include('settings.webhooks.parts.webhooks-list-item', ['webhook' => $webhook])
@endforeach
-
+
@else
{{ trans('settings.webhooks_none_created') }}
@endif
+
+ {{ $webhooks->links() }}
+
diff --git a/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php b/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php
new file mode 100644
index 000000000..0ba613196
--- /dev/null
+++ b/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php
@@ -0,0 +1,18 @@
+
+
+
+ @include('common.status-indicator', ['status' => $webhook->active])
+
+
+
+ @if($webhook->tracksEvent('all'))
+ {{ trans('settings.webhooks_events_all') }}
+ @else
+ {{ trans_choice('settings.webhooks_x_trigger_events', $webhook->tracked_events_count, ['count' => $webhook->tracked_events_count]) }}
+ @endif
+
+
+
+ {{ $webhook->endpoint }}
+
+
\ No newline at end of file
diff --git a/resources/views/shelves/index.blade.php b/resources/views/shelves/index.blade.php
index ee52769aa..75d46318f 100644
--- a/resources/views/shelves/index.blade.php
+++ b/resources/views/shelves/index.blade.php
@@ -1,7 +1,7 @@
@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')
diff --git a/resources/views/shelves/parts/list.blade.php b/resources/views/shelves/parts/list.blade.php
index d78606ac7..da9c06d92 100644
--- a/resources/views/shelves/parts/list.blade.php
+++ b/resources/views/shelves/parts/list.blade.php
@@ -1,10 +1,9 @@
-
{{ trans('entities.shelves') }}
- @include('entities.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'bookshelves'])
+ @include('common.sort', $listOptions->getSortControlData())
@@ -31,7 +30,8 @@
@else
{{ trans('entities.shelves_empty') }}
@if(userCan('bookshelf-create-all'))
- @icon('edit'){{ trans('entities.create_now') }}
+ @icon('edit'){{ trans('entities.create_now') }}
@endif
@endif
diff --git a/resources/views/shelves/show.blade.php b/resources/views/shelves/show.blade.php
index 37d288956..0195759d8 100644
--- a/resources/views/shelves/show.blade.php
+++ b/resources/views/shelves/show.blade.php
@@ -23,12 +23,7 @@
{{ $shelf->name }}
- @include('entities.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())
diff --git a/resources/views/tags/index.blade.php b/resources/views/tags/index.blade.php
index c88449ce7..b6b3325e0 100644
--- a/resources/views/tags/index.blade.php
+++ b/resources/views/tags/index.blade.php
@@ -5,25 +5,28 @@
-
-
{{ trans('entities.tags') }}
+
{{ trans('entities.tags') }}
-
-
-
- @include('form.request-query-inputs', ['params' => ['name']])
-
-
-
+
{{ trans('entities.tags_index_desc') }}
+
+
+
+
+ @include('form.request-query-inputs', ['params' => ['name']])
+
+
+
+
+ @include('common.sort', $listOptions->getSortControlData())
@if($nameFilter)
-
-
{{ trans('common.filter_active') }}
+
+
{{ trans('common.filter_active') }}
@include('entities.tag', ['tag' => new \BookStack\Actions\Tag(['name' => $nameFilter])])
@include('form.request-query-inputs', ['params' => ['search']])
@@ -33,13 +36,13 @@
@endif
@if(count($tags) > 0)
-
+
@foreach($tags as $tag)
- @include('tags.parts.table-row', ['tag' => $tag, 'nameFilter' => $nameFilter])
+ @include('tags.parts.tags-list-item', ['tag' => $tag, 'nameFilter' => $nameFilter])
@endforeach
-
+
-
+
{{ $tags->links() }}
@else
diff --git a/resources/views/tags/parts/table-row.blade.php b/resources/views/tags/parts/table-row.blade.php
deleted file mode 100644
index aa04959a9..000000000
--- a/resources/views/tags/parts/table-row.blade.php
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
- @include('entities.tag', ['tag' => $tag])
-
-
- @icon('leaderboard'){{ $tag->usages }}
-
-
- @icon('page'){{ $tag->page_count }}
-
-
- @icon('chapter'){{ $tag->chapter_count }}
-
-
- @icon('book'){{ $tag->book_count }}
-
-
- @icon('bookshelf'){{ $tag->shelf_count }}
-
-
- @if($tag->values ?? false)
- {{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }}
- @elseif(empty($nameFilter))
- {{ trans('entities.tags_all_values') }}
- @endif
-
-
\ No newline at end of file
diff --git a/resources/views/tags/parts/tags-list-item.blade.php b/resources/views/tags/parts/tags-list-item.blade.php
new file mode 100644
index 000000000..3962db760
--- /dev/null
+++ b/resources/views/tags/parts/tags-list-item.blade.php
@@ -0,0 +1,31 @@
+
+
+ @include('entities.tag', ['tag' => $tag])
+
+
+ @if($tag->values ?? false)
+
+ @elseif(empty($nameFilter))
+
+ @endif
+
\ No newline at end of file
diff --git a/resources/views/users/api-tokens/parts/list.blade.php b/resources/views/users/api-tokens/parts/list.blade.php
index ea1893372..58617fb85 100644
--- a/resources/views/users/api-tokens/parts/list.blade.php
+++ b/resources/views/users/api-tokens/parts/list.blade.php
@@ -1,6 +1,6 @@
-
-
{{ trans('settings.users_api_tokens') }}
+
@if (count($user->apiTokens) > 0)
-
-
- {{ trans('common.name') }}
- {{ trans('settings.users_api_tokens_expires') }}
-
-
+
@foreach($user->apiTokens as $token)
-
-
- {{ $token->name }}
+
- {{ $token->expires_at->format('Y-m-d') ?? '' }}
-
- {{ trans('common.edit') }}
-
-
+
+
+
+ {{ trans('settings.users_api_tokens_expires') }}
+ {{ $token->expires_at->format('Y-m-d') ?? '' }}
+
+
+
+
@endforeach
-
+
@else
{{ trans('settings.users_api_tokens_none') }}
@endif
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php
index 03eae2c00..0dd607f8c 100644
--- a/resources/views/users/index.blade.php
+++ b/resources/views/users/index.blade.php
@@ -9,59 +9,34 @@
{{ trans('settings.users') }}
-
-
+
{{ $users->links() }}
diff --git a/resources/views/users/parts/users-list-item.blade.php b/resources/views/users/parts/users-list-item.blade.php
new file mode 100644
index 000000000..dc7c9f272
--- /dev/null
+++ b/resources/views/users/parts/users-list-item.blade.php
@@ -0,0 +1,27 @@
+
+
+
+
+
+ @if($user->last_activity_at)
+ {{ trans('settings.users_latest_activity') }}
+
+ {{ $user->last_activity_at->diffForHumans() }}
+ @endif
+
+
+
\ No newline at end of file
diff --git a/routes/web.php b/routes/web.php
index 1cffbfd7d..b3f11f53a 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -29,6 +29,7 @@ use BookStack\Http\Controllers\StatusController;
use BookStack\Http\Controllers\TagController;
use BookStack\Http\Controllers\UserApiTokenController;
use BookStack\Http\Controllers\UserController;
+use BookStack\Http\Controllers\UserPreferencesController;
use BookStack\Http\Controllers\UserProfileController;
use BookStack\Http\Controllers\UserSearchController;
use BookStack\Http\Controllers\WebhookController;
@@ -239,18 +240,20 @@ Route::middleware('auth')->group(function () {
Route::get('/settings/users', [UserController::class, 'index']);
Route::get('/settings/users/create', [UserController::class, 'create']);
Route::get('/settings/users/{id}/delete', [UserController::class, 'delete']);
- Route::patch('/settings/users/{id}/switch-books-view', [UserController::class, 'switchBooksView']);
- Route::patch('/settings/users/{id}/switch-shelves-view', [UserController::class, 'switchShelvesView']);
- Route::patch('/settings/users/{id}/switch-shelf-view', [UserController::class, 'switchShelfView']);
- Route::patch('/settings/users/{id}/change-sort/{type}', [UserController::class, 'changeSort']);
- Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserController::class, 'updateExpansionPreference']);
- Route::patch('/settings/users/toggle-dark-mode', [UserController::class, 'toggleDarkMode']);
- Route::patch('/settings/users/update-code-language-favourite', [UserController::class, 'updateCodeLanguageFavourite']);
Route::post('/settings/users/create', [UserController::class, 'store']);
Route::get('/settings/users/{id}', [UserController::class, 'edit']);
Route::put('/settings/users/{id}', [UserController::class, 'update']);
Route::delete('/settings/users/{id}', [UserController::class, 'destroy']);
+ // User Preferences
+ Route::patch('/settings/users/{id}/switch-books-view', [UserPreferencesController::class, 'switchBooksView']);
+ Route::patch('/settings/users/{id}/switch-shelves-view', [UserPreferencesController::class, 'switchShelvesView']);
+ Route::patch('/settings/users/{id}/switch-shelf-view', [UserPreferencesController::class, 'switchShelfView']);
+ Route::patch('/settings/users/{id}/change-sort/{type}', [UserPreferencesController::class, 'changeSort']);
+ Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserPreferencesController::class, 'updateExpansionPreference']);
+ Route::patch('/settings/users/toggle-dark-mode', [UserPreferencesController::class, 'toggleDarkMode']);
+ Route::patch('/settings/users/update-code-language-favourite', [UserPreferencesController::class, 'updateCodeLanguageFavourite']);
+
// User API Tokens
Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']);
Route::post('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'store']);
diff --git a/tests/Actions/AuditLogTest.php b/tests/Actions/AuditLogTest.php
index 987e23a45..25fa2b796 100644
--- a/tests/Actions/AuditLogTest.php
+++ b/tests/Actions/AuditLogTest.php
@@ -51,7 +51,7 @@ class AuditLogTest extends TestCase
$resp->assertSeeText($page->name);
$resp->assertSeeText('page_create');
$resp->assertSeeText($activity->created_at->toDateTimeString());
- $this->withHtml($resp)->assertElementContains('.table-user-item', $admin->name);
+ $this->withHtml($resp)->assertElementContains('a[href*="users/' . $admin->id . '"]', $admin->name);
}
public function test_shows_name_for_deleted_items()
diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php
index d00ec5ce5..0749888c8 100644
--- a/tests/Entity/PageRevisionTest.php
+++ b/tests/Entity/PageRevisionTest.php
@@ -195,12 +195,12 @@ class PageRevisionTest extends TestCase
$this->createRevisions($page, 1, ['html' => 'new page html']);
$resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions'));
- $this->withHtml($resp)->assertElementContains('td', '(WYSIWYG)');
- $this->withHtml($resp)->assertElementNotContains('td', '(Markdown)');
+ $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'WYSIWYG)');
+ $this->withHtml($resp)->assertElementNotContains('.item-list-row > div:nth-child(2)', 'Markdown)');
$this->createRevisions($page, 1, ['markdown' => '# Some markdown content']);
$resp = $this->get($page->refresh()->getUrl('/revisions'));
- $this->withHtml($resp)->assertElementContains('td', '(Markdown)');
+ $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'Markdown)');
}
public function test_revision_restore_action_only_visible_with_permission()
diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php
index ed5c798a5..ab06686e0 100644
--- a/tests/Entity/TagTest.php
+++ b/tests/Entity/TagTest.php
@@ -164,7 +164,7 @@ class TagTest extends TestCase
$resp->assertSee('OtherTestContent');
$resp->assertDontSee('OtherTagName');
$resp->assertSee('Active Filter:');
- $this->withHtml($resp)->assertElementCount('table .tag-item', 2);
+ $this->withHtml($resp)->assertElementCount('.item-list .tag-item', 2);
$this->withHtml($resp)->assertElementContains('form[action$="/tags"]', 'Clear Filter');
}
diff --git a/tests/Settings/RecycleBinTest.php b/tests/Settings/RecycleBinTest.php
index 3d27e9c8d..990df607e 100644
--- a/tests/Settings/RecycleBinTest.php
+++ b/tests/Settings/RecycleBinTest.php
@@ -62,11 +62,11 @@ class RecycleBinTest extends TestCase
$viewReq = $this->asAdmin()->get('/settings/recycle-bin');
$html = $this->withHtml($viewReq);
- $html->assertElementContains('table.table', $page->name);
- $html->assertElementContains('table.table', $editor->name);
- $html->assertElementContains('table.table', $book->name);
- $html->assertElementContains('table.table', $book->pages_count . ' Pages');
- $html->assertElementContains('table.table', $book->chapters_count . ' Chapters');
+ $html->assertElementContains('.item-list-row', $page->name);
+ $html->assertElementContains('.item-list-row', $editor->name);
+ $html->assertElementContains('.item-list-row', $book->name);
+ $html->assertElementContains('.item-list-row', $book->pages_count . ' Pages');
+ $html->assertElementContains('.item-list-row', $book->chapters_count . ' Chapters');
}
public function test_recycle_bin_empty()
diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php
index c65b11d7d..92e4158cd 100644
--- a/tests/User/UserPreferencesTest.php
+++ b/tests/User/UserPreferencesTest.php
@@ -29,21 +29,6 @@ class UserPreferencesTest extends TestCase
$this->assertEquals('desc', setting()->getForCurrentUser('books_sort_order'));
}
- public function test_update_sort_preference_defaults()
- {
- $editor = $this->getEditor();
- $this->actingAs($editor);
-
- $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/bookshelves', [
- 'sort' => 'cat',
- 'order' => 'dog',
- ]);
- $updateRequest->assertStatus(302);
-
- $this->assertEquals('name', setting()->getForCurrentUser('bookshelves_sort'));
- $this->assertEquals('asc', setting()->getForCurrentUser('bookshelves_sort_order'));
- }
-
public function test_update_sort_bad_entity_type_handled()
{
$editor = $this->getEditor();