]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #3821 from BookStackApp/list_reworks
authorDan Brown <redacted>
Thu, 3 Nov 2022 14:52:40 +0000 (14:52 +0000)
committerGitHub <redacted>
Thu, 3 Nov 2022 14:52:40 +0000 (14:52 +0000)
Revision of item list views

64 files changed:
.github/workflows/analyse-php.yml
.github/workflows/test-migrations.yml
.github/workflows/test-php.yml
app/Actions/Queries/WebhooksAllPaginatedAndSorted.php [new file with mode: 0644]
app/Actions/TagRepo.php
app/Auth/Queries/RolesAllPaginatedAndSorted.php [new file with mode: 0644]
app/Auth/Queries/UsersAllPaginatedAndSorted.php [moved from app/Auth/Queries/AllUsersPaginatedAndSorted.php with 63% similarity]
app/Auth/Role.php
app/Http/Controllers/AuditLogController.php
app/Http/Controllers/BookController.php
app/Http/Controllers/BookshelfController.php
app/Http/Controllers/HomeController.php
app/Http/Controllers/PageRevisionController.php
app/Http/Controllers/RoleController.php
app/Http/Controllers/TagController.php
app/Http/Controllers/UserController.php
app/Http/Controllers/UserPreferencesController.php [new file with mode: 0644]
app/Http/Controllers/WebhookController.php
app/Util/SimpleListOptions.php [new file with mode: 0644]
resources/js/components/entity-permissions.js
resources/js/components/list-sort-control.js
resources/js/components/permissions-table.js
resources/lang/en/entities.php
resources/lang/en/settings.php
resources/sass/_blocks.scss
resources/sass/_components.scss
resources/sass/_layout.scss
resources/sass/_opacity.scss [new file with mode: 0644]
resources/sass/styles.scss
resources/views/books/index.blade.php
resources/views/books/parts/list.blade.php
resources/views/common/sort.blade.php [moved from resources/views/entities/sort.blade.php with 52% similarity]
resources/views/common/status-indicator.blade.php [new file with mode: 0644]
resources/views/form/entity-permissions-row.blade.php
resources/views/form/entity-permissions.blade.php
resources/views/pages/parts/revisions-index-row.blade.php [moved from resources/views/pages/parts/revision-table-row.blade.php with 62% similarity]
resources/views/pages/revisions.blade.php
resources/views/search/parts/term-list.blade.php
resources/views/settings/audit.blade.php
resources/views/settings/parts/table-user.blade.php
resources/views/settings/recycle-bin/index.blade.php
resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php [new file with mode: 0644]
resources/views/settings/roles/index.blade.php
resources/views/settings/roles/parts/asset-permissions-row.blade.php [new file with mode: 0644]
resources/views/settings/roles/parts/form.blade.php
resources/views/settings/roles/parts/related-asset-permissions-row.blade.php [new file with mode: 0644]
resources/views/settings/roles/parts/roles-list-item.blade.php [new file with mode: 0644]
resources/views/settings/webhooks/index.blade.php
resources/views/settings/webhooks/parts/webhooks-list-item.blade.php [new file with mode: 0644]
resources/views/shelves/index.blade.php
resources/views/shelves/parts/list.blade.php
resources/views/shelves/show.blade.php
resources/views/tags/index.blade.php
resources/views/tags/parts/table-row.blade.php [deleted file]
resources/views/tags/parts/tags-list-item.blade.php [new file with mode: 0644]
resources/views/users/api-tokens/parts/list.blade.php
resources/views/users/index.blade.php
resources/views/users/parts/users-list-item.blade.php [new file with mode: 0644]
routes/web.php
tests/Actions/AuditLogTest.php
tests/Entity/PageRevisionTest.php
tests/Entity/TagTest.php
tests/Settings/RecycleBinTest.php
tests/User/UserPreferencesTest.php

index 191399d78b8a54f8565c06619a3767dffd58f496..fd56a53ef9ac645dd81a0def99862914b9cae77d 100644 (file)
@@ -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
index e9b66a0a65d09001372084761589ab66be8ba851..d762d7eab3e2a258fb41a06ca4de5e7a9a7c5a94 100644 (file)
@@ -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 }}
index 917038f599dbf97896e2abedcbdacaba8dff75d7..4185e83c31071a25b65910e30de216a2a4f6dc26 100644 (file)
@@ -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 (file)
index 0000000..4958b60
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace BookStack\Actions\Queries;
+
+use BookStack\Actions\Webhook;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+/**
+ * Get all the webhooks in the system in a paginated format.
+ */
+class WebhooksAllPaginatedAndSorted
+{
+    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
+    {
+        $query = Webhook::query()->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);
+    }
+}
index 2618ed2e902329512defd05545e355bc22c9c286..cece30de003b3a48b0d72f2d0e30b28e7dfa828e 100644 (file)
@@ -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 (file)
index 0000000..9ee4f6c
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace BookStack\Auth\Queries;
+
+use BookStack\Auth\Role;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+/**
+ * Get all the roles in the system in a paginated format.
+ */
+class RolesAllPaginatedAndSorted
+{
+    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
+    {
+        $sort = $listOptions->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);
+    }
+}
similarity index 63%
rename from app/Auth/Queries/AllUsersPaginatedAndSorted.php
rename to app/Auth/Queries/UsersAllPaginatedAndSorted.php
index 7b849eaf4c1af9f84036cf8eeb1aa24f89716836..29b6a89697bc880daf0788f66158fe8431b861db 100644 (file)
@@ -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);
index 17a4edcc020b0a84c5e1da880d364747069e2abc..b293d1af256aabd1d01574c4732ad729c698401a 100644 (file)
@@ -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}
      */
index ec3f3697534fcabe6e74ddf4759e610160bcca75..da8009d78cb18b9665f84031b69da4e4e5d1fa12 100644 (file)
@@ -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,
         ]);
     }
index b323ae496e42586a1702c9dafea528a0810837e5..14c3af1cc5cf8f0a8c92d70de7c24d8a1d608c96 100644 (file)
@@ -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,
         ]);
     }
 
index 3c63be6318b92b28e1c5ed3e6584538d0bfad6df..537ea915b76130deb086c934b0c22516e070dd07 100644 (file)
@@ -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),
         ]);
     }
index f38bd71dfc7ea456a9e5e46f74140035ba777c91..c3c8d1066d75dd664cad9750a45f955905c651dc 100644 (file)
@@ -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);
index 85ee6c2bcccbaa0e1ed35ab63e2dec1e10698515..3da5e7c2dd07894c6ed67389d30cc5746b43fe86 100644 (file)
@@ -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,
         ]);
     }
 
index fee31ffbfe297a8806a9b9d12a8c310c3325418a..a9be19e0cc7276de07e3c682638caea80db18fc1 100644 (file)
@@ -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'));
 
index 056cc9902d564f4204899a08e0b4b7af3bd8725c..6c2876043c4fa78e24c959bf1ca00442183d5b58 100644 (file)
@@ -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,
         ]);
     }
 
index 895481d02405305c80198541883613941a69a13a..f69f00cf79e602217e4971e4db0992721b87760b 100644 (file)
@@ -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 (file)
index 0000000..972742e
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Auth\UserRepo;
+use Illuminate\Http\Request;
+
+class UserPreferencesController extends Controller
+{
+    protected UserRepo $userRepo;
+
+    public function __construct(UserRepo $userRepo)
+    {
+        $this->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));
+    }
+}
index 264921dfc39ed7743f1f58f27afe5d400196a2a9..c72dcc51066d4af80bef78c6b2cb9340d1960962 100644 (file)
@@ -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 (file)
index 0000000..81d8a58
--- /dev/null
@@ -0,0 +1,104 @@
+<?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, 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,
+        ];
+    }
+}
index c67c85f19a699ef43fad99c7d300b6f4b8851b58..0dec5ca0937dda6620122628e5aef22b5ccd8013 100644 (file)
@@ -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;
 
index 23fc64ae6a47cde19602f4af610c75926582d275..3b642dbde09604485d78f81b87f280c42f2daceb 100644 (file)
@@ -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();
     }
index df3c055cafa037f59a551b7887a3fb0f355646f4..d33c9928f9412998be6523f2244fcb0bc32b30c3 100644 (file)
@@ -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) {
index bf6201900fc3653724e85d715411695325b6fd0a..e7fbe37d95db6a0d52a6658b8a6e906a600e6587 100644 (file)
@@ -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.",
index 1ad271e7c9ebd736f1d283bc2884b756294ea56d..f4204dd68bf05858a70205983d8aa35204366efa 100755 (executable)
@@ -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',
index 0398224ca52a406ee29d7ae160fa63a00caae4b4..6058add828e2eccef4d46ca8465439a78277c5db 100644 (file)
   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
  */
index 9fdd5a6117eb2b650965603bc2fac4159bc717db..acb45100f61da21b0c5a90cd5c313d760287e95f 100644 (file)
@@ -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
index cfb8397c99207e4ba3a436ed35c556c39caab22f..a5f895f80de9a4ddcc95037663a68dd6a44eb4f0 100644 (file)
@@ -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 (file)
index 0000000..235aed4
--- /dev/null
@@ -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
index ab97466a5aa260e50c7e24a09c04d6526f82b810..5e31dbdfb577016667bdb15c901f3d11c33a8325 100644 (file)
@@ -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
index 6573bbe6a87adef2838aa2808d863fd08b96d7d6..447d6fd44c21a3d5a313c9da913e778da2cecaaf 100644 (file)
@@ -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')
index 30b0766135ccff81c87fb2d3fa7c1a5ba9d2de4a..2cf83dfa91b6d55d16c6becd2435dbb5c0267480 100644 (file)
@@ -2,13 +2,7 @@
     <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('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())
         </div>
     </div>
     @if(count($books) > 0)
                 @endforeach
             </div>
         @else
-             <div class="grid third">
+            <div class="grid third">
                 @foreach($books as $key => $book)
                     @include('entities.grid-item', ['entity' => $book])
                 @endforeach
-             </div>
+            </div>
         @endif
         <div>
             {!! $books->render() !!}
similarity index 52%
rename from resources/views/entities/sort.blade.php
rename to resources/views/common/sort.blade.php
index f81ed797f652f940a28cef225357ad751e3360a1..996f7a8376118c6d207c56e56d5aa5f07f35a89f 100644 (file)
@@ -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';
 ?>
-<div class="list-sort-container" list-sort-control>
+<div component="list-sort-control" class="list-sort-container">
     <div class="list-sort-label">{{ trans('common.sort') }}</div>
-    <form action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}" method="post">
+    <form refs="list-sort-control@form"
+          @if($useQuery ?? false)
+              action="{{ url()->current() }}"
+              method="get"
+          @else
+              action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}"
+              method="post"
+          @endif
+    >
 
-        {!! csrf_field() !!}
-        {!! method_field('PATCH') !!}
-        <input type="hidden" value="{{ $selectedSort }}" name="sort">
-        <input type="hidden" value="{{ $order }}" name="order">
+        @if($useQuery ?? false)
+            @foreach(array_filter(request()->except(['sort', 'order'])) as $key => $value)
+                <input type="hidden" name="{{ $key }}" value="{{ $value }}">
+            @endforeach
+        @else
+            {!! method_field('PATCH') !!}
+            {!! csrf_field() !!}
+        @endif
+
+        <input refs="list-sort-control@sort" type="hidden" value="{{ $selectedSort }}" name="sort">
+        <input refs="list-sort-control@order" type="hidden" value="{{ $order }}" name="order">
 
         <div class="list-sort">
             <div component="dropdown" class="list-sort-type dropdown-container">
                 <div refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
-                <ul refs="dropdown@menu" class="dropdown-menu">
+                <ul refs="dropdown@menu list-sort-control@menu" class="dropdown-menu">
                     @foreach($options as $key => $label)
                         <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}" class="text-item">{{ $label }}</a></li>
                     @endforeach
                 </ul>
             </div>
-            <button href="#" class="list-sort-dir" type="button" data-sort-dir
+            <button class="list-sort-dir" type="button" data-sort-dir
                     aria-label="{{ trans('common.sort_direction_toggle') }} - {{ $order === 'asc' ? trans('common.sort_ascending') : trans('common.sort_descending') }}" tabindex="0">
                 @icon($order === 'desc' ? 'sort-up' : 'sort-down')
             </button>
diff --git a/resources/views/common/status-indicator.blade.php b/resources/views/common/status-indicator.blade.php
new file mode 100644 (file)
index 0000000..ba9b1b4
--- /dev/null
@@ -0,0 +1,3 @@
+<span title="{{ trans('common.status_' . ($status ? 'active' : 'inactive')) }}"
+      class="status-indicator-{{ $status ? 'active' : 'inactive' }}"
+></span>
\ No newline at end of file
index d2e6a475631ce6a9becada220d9d4c0b8403ccd0..d4c6c4ac12216c4df9c7cb596e50042a7e105dfa 100644 (file)
@@ -5,7 +5,7 @@ $permission - The entity permission containing the permissions.
 $inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for "Everyone Else" role.
 --}}
 
-<div component="permissions-table" class="content-permissions-row flex-container-row justify-space-between wrap">
+<div component="permissions-table" class="item-list-row flex-container-row justify-space-between wrap">
     <div class="gap-x-m flex-container-row items-center px-l py-m flex">
         <div class="text-large" title="{{ $role->id === 0 ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}">
             @icon($role->id === 0 ? 'groups' : 'role')
@@ -16,7 +16,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
         </span>
         @if($role->id !== 0)
             <button type="button"
-                class="ml-auto flex-none text-small text-primary text-button hover-underline content-permissions-row-toggle-all hide-under-s"
+                class="ml-auto flex-none text-small text-primary text-button hover-underline item-list-row-toggle-all hide-under-s"
                 refs="permissions-table@toggle-all"
                 ><strong>{{ trans('common.toggle_all') }}</strong></button>
         @endif
index 724d0fb393658b4f5152331d8ba3117d4386ca2b..9bf309fb802952f16f9dabee3f076006087cac63 100644 (file)
@@ -35,7 +35,7 @@
 
     <hr>
 
-    <div refs="entity-permissions@role-container" class="content-permissions mt-m mb-m">
+    <div refs="entity-permissions@role-container" class="item-list mt-m mb-m">
         @foreach($data->permissionsWithRoles() as $permission)
             @include('form.entity-permissions-row', [
                 'permission' => $permission,
@@ -58,7 +58,7 @@
         </div>
     </div>
 
-    <div class="content-permissions mt-m mb-xl">
+    <div class="item-list mt-m mb-xl">
         @include('form.entity-permissions-row', [
                 'role' => $data->everyoneElseRole(),
                 'permission' => $data->everyoneElseEntityPermission(),
similarity index 62%
rename from resources/views/pages/parts/revision-table-row.blade.php
rename to resources/views/pages/parts/revisions-index-row.blade.php
index 24301adc376991a9bbe4412440f627fd2ee14388..597b53234198cef25addab495078237721302a61 100644 (file)
@@ -1,38 +1,43 @@
-<tr>
-    <td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
-    <td>
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="flex fit-content min-width-xxxxs px-m py-xs">
+        <span class="hide-over-l">{{ trans('entities.pages_revisions_number') }}</span>
+        {{ $revision->revision_number == 0 ? '' : $revision->revision_number }}
+    </div>
+    <div class="flex-2 px-m py-xs min-width-s">
         {{ $revision->name }}
         <br>
-        <small class="text-muted">({{ $revision->is_markdown ? 'Markdown' : 'WYSIWYG' }})</small>
-    </td>
-    <td style="line-height: 0;" width="30">
-        @if($revision->createdBy)
-            <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
-        @endif
-    </td>
-    <td width="260">
-        @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
-        <br>
-        <div class="text-muted">
-            <small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
-            <small>({{ $revision->created_at->diffForHumans() }})</small>
+        <small class="text-muted">(<strong class="hide-over-l">{{ trans('entities.pages_revisions_editor') }}: </strong>{{ $revision->is_markdown ? 'Markdown' : 'WYSIWYG' }})</small>
+    </div>
+    <div class="flex-3 px-m py-xs min-width-l">
+        <div class="flex-container-row items-center gap-s">
+            @if($revision->createdBy)
+                <img class="avatar flex-none" height="30" width="30" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
+            @endif
+            <div>
+                @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
+                <br>
+                <div class="text-muted">
+                    <small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
+                    <small>({{ $revision->created_at->diffForHumans() }})</small>
+                </div>
+            </div>
         </div>
-    </td>
-    <td>
+    </div>
+    <div class="flex-2 px-m py-xs min-width-m text-small">
         {{ $revision->summary }}
-    </td>
-    <td class="actions text-small text-right">
+    </div>
+    <div class="flex-2 px-m py-xs actions text-small text-l-right min-width-l">
         <a href="{{ $revision->getUrl('changes') }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_changes') }}</a>
-        <span class="text-muted">&nbsp;|&nbsp;</span>
+        <span class="text-muted opacity-70">&nbsp;|&nbsp;</span>
 
 
-        @if ($index === 0)
+        @if ($current)
             <a target="_blank" rel="noopener" href="{{ $revision->page->getUrl() }}"><i>{{ trans('entities.pages_revisions_current') }}</i></a>
         @else
             <a href="{{ $revision->getUrl() }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_preview') }}</a>
 
             @if(userCan('page-update', $revision->page))
-                <span class="text-muted">&nbsp;|&nbsp;</span>
+                <span class="text-muted opacity-70">&nbsp;|&nbsp;</span>
                 <div component="dropdown" class="dropdown-container">
                     <a refs="dropdown@toggle" href="#" aria-haspopup="true" aria-expanded="false">{{ trans('entities.pages_revisions_restore') }}</a>
                     <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
@@ -52,7 +57,7 @@
             @endif
 
             @if(userCan('page-delete', $revision->page))
-                <span class="text-muted">&nbsp;|&nbsp;</span>
+                <span class="text-muted opacity-70">&nbsp;|&nbsp;</span>
                 <div component="dropdown" class="dropdown-container">
                     <a refs="dropdown@toggle" href="#" aria-haspopup="true" aria-expanded="false">{{ trans('common.delete') }}</a>
                     <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
@@ -71,5 +76,5 @@
                 </div>
             @endif
         @endif
-    </td>
-</tr>
\ No newline at end of file
+    </div>
+</div>
\ No newline at end of file
index 3e7edad997fe1c8cdb823abd2762ca67475a7d64..9f462e930ebb6e7e054a303a2664f807ada4948a 100644 (file)
 
         <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.pages_revisions') }}</h1>
-            @if(count($revisions) > 0)
 
-                <table class="table">
-                    <tr>
-                        <th width="56">{{ trans('entities.pages_revisions_number') }}</th>
-                        <th>
-                            {{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}
-                        </th>
-                        <th colspan="2">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</th>
-                        <th>{{ trans('entities.pages_revisions_changelog') }}</th>
-                        <th class="text-right">{{ trans('common.actions') }}</th>
-                    </tr>
+            <p class="text-muted">{{ trans('entities.pages_revisions_desc') }}</p>
+
+            <div class="flex-container-row my-m items-center justify-space-between wrap gap-x-m gap-y-s">
+                {{ $revisions->links() }}
+                <div>
+                    @include('common.sort', $listOptions->getSortControlData())
+                </div>
+            </div>
+
+            @if(count($revisions) > 0)
+                <div class="item-list">
+                    <div class="item-list-row flex-container-row items-center strong hide-under-l">
+                        <div class="flex fit-content min-width-xxxxs px-m py-xs">{{ trans('entities.pages_revisions_number') }}</div>
+                        <div class="flex-2 px-m py-xs">{{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}</div>
+                        <div class="flex-3 px-m py-xs">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</div>
+                        <div class="flex-2 px-m py-xs">{{ trans('entities.pages_revisions_changelog') }}</div>
+                        <div class="flex-2 px-m py-xs text-right">{{ trans('common.actions') }}</div>
+                    </div>
                     @foreach($revisions as $index => $revision)
-                        @include('pages.parts.revision-table-row', ['revision' => $revision])
+                        @include('pages.parts.revisions-index-row', ['revision' => $revision, 'current' => $page->revision_count === $revision->revision_number])
                     @endforeach
-                </table>
-
+                </div>
             @else
                 <p>{{ trans('entities.pages_revisions_none') }}</p>
             @endif
+
+            <div class="my-m">
+                {{ $revisions->links() }}
+            </div>
         </main>
 
     </div>
index 3fbfa18fef25fadf32adb306ceb6401bcc964df9..dfcc80269e6fee6d5d5712e5b7235d79e9ef6b99 100644 (file)
@@ -2,25 +2,24 @@
 @type - Type of term (exact, tag)
 @currentList
 --}}
-<table component="add-remove-rows"
+<div component="add-remove-rows"
        option:add-remove-rows:remove-selector="button.text-neg"
-       option:add-remove-rows:row-selector="tr"
-       class="no-style">
+       option:add-remove-rows:row-selector=".flex-container-row"
+        class="flex-container-column gap-xs">
     @foreach(array_merge($currentList, ['']) as $term)
-        <tr @if(empty($term)) class="hidden" refs="add-remove-rows@model" @endif>
-            <td class="pb-s pr-m">
+        <div @if(empty($term)) refs="add-remove-rows@model" @endif
+            class="{{ $term ? '' : 'hidden' }} flex-container-row items-center gap-x-xs">
+            <div>
                 <input class="exact-input outline" type="text" name="{{$type}}[]" value="{{ $term }}">
-            </td>
-            <td>
-                <button type="button" class="text-neg text-button">@icon('close')</button>
-            </td>
-        </tr>
+            </div>
+            <div>
+                <button type="button" class="text-neg text-button icon-button p-xs">@icon('close')</button>
+            </div>
+        </div>
     @endforeach
-    <tr>
-        <td colspan="2">
-            <button refs="add-remove-rows@add" type="button" class="text-button">
-                @icon('add-circle'){{ trans('common.add') }}
-            </button>
-        </td>
-    </tr>
-</table>
\ No newline at end of file
+    <div class="flex py-xs">
+        <button refs="add-remove-rows@add" type="button" class="text-button">
+            @icon('add-circle'){{ trans('common.add') }}
+        </button>
+    </div>
+</div>
\ No newline at end of file
index 2daeb8a8253bca070997fccdb164120e6f5d0908..abb9c2771116bfe4363b74e6d6ffdee128970855 100644 (file)
@@ -9,7 +9,11 @@
         <h1 class="list-heading">{{ trans('settings.audit') }}</h1>
         <p class="text-muted">{{ trans('settings.audit_desc') }}</p>
 
-        <form action="{{ url('/settings/audit') }}" method="get" class="flex-container-row wrap justify-flex-start gap-m">
+        <form action="{{ url('/settings/audit') }}" method="get" class="flex-container-row wrap justify-flex-start gap-x-m gap-y-xs">
+
+            @foreach(request()->only(['order', 'sort']) as $key => $val)
+                <input type="hidden" name="{{ $key }}" value="{{ $val }}">
+            @endforeach
 
             <div component="dropdown" class="list-sort-type dropdown-container">
                 <label for="">{{ trans('settings.audit_event_filter') }}</label>
                         aria-haspopup="true"
                         aria-expanded="false"
                         aria-label="{{ trans('common.sort_options') }}"
-                        class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
+                        class="input-base text-left">{{ $filters['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
                 <ul refs="dropdown@menu" class="dropdown-menu">
-                    <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
+                    <li @if($filters['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', array_filter(request()->except('page')), ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
                     @foreach($activityTypes as $type)
-                        <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
+                        <li @if($type === $filters['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', array_filter(request()->except('page')), ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
                     @endforeach
                 </ul>
             </div>
 
-            @if(!empty($listDetails['event']))
-                <input type="hidden" name="event" value="{{ $listDetails['event'] }}">
+            @if(!empty($filters['event']))
+                <input type="hidden" name="event" value="{{ $filters['event'] }}">
             @endif
 
             @foreach(['date_from', 'date_to'] as $filterKey)
@@ -38,7 +42,7 @@
                            component="submit-on-change"
                            type="date"
                            name="{{ $filterKey }}"
-                           value="{{ $listDetails[$filterKey] ?? '' }}">
+                           value="{{ $filters[$filterKey] ?? '' }}">
                 </div>
             @endforeach
 
                  component="submit-on-change"
                  option:submit-on-change:filter='[name="user"]'>
                 <label for="owner">{{ trans('settings.audit_table_user') }}</label>
-                @include('form.user-select', ['user' => $listDetails['user'] ? \BookStack\Auth\User::query()->find($listDetails['user']) : null, 'name' => 'user'])
+                @include('form.user-select', ['user' => $filters['user'] ? \BookStack\Auth\User::query()->find($filters['user']) : null, 'name' => 'user'])
             </div>
 
 
             <div class="form-group">
                 <label for="ip">{{ trans('settings.audit_table_ip') }}</label>
-                @include('form.text', ['name' => 'ip', 'model' => (object) $listDetails])
+                @include('form.text', ['name' => 'ip', 'model' => (object) $filters])
                 <input type="submit" style="display: none">
             </div>
         </form>
 
-        <hr class="mt-l mb-s">
-
-        {{ $activities->links() }}
-
-        <table class="table">
-            <tbody>
-            <tr>
-                <th>{{ trans('settings.audit_table_user') }}</th>
-                <th>
-                    <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'key']) }}">{{ trans('settings.audit_table_event') }}</a>
-                </th>
-                <th>{{ trans('settings.audit_table_related') }}</th>
-                <th>{{ trans('settings.audit_table_ip') }}</th>
-                <th>
-                    <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'created_at']) }}">{{ trans('settings.audit_table_date') }}</a></th>
-            </tr>
+        <hr class="mt-m mb-s">
+
+        <div class="flex-container-row justify-space-between items-center wrap">
+            <div class="flex-2 min-width-xl">{{ $activities->links() }}</div>
+            <div class="flex-none min-width-m py-m">
+                @include('common.sort', array_merge($listOptions->getSortControlData(), ['useQuery' => true]))
+            </div>
+        </div>
+
+        <div class="item-list">
+            <div class="item-list-row flex-container-row items-center bold hide-under-m">
+                <div class="flex-2 px-m py-xs flex-container-row items-center">{{ trans('settings.audit_table_user') }}</div>
+                <div class="flex-2 px-m py-xs">{{ trans('settings.audit_table_event') }}</div>
+                <div class="flex-3 px-m py-xs">{{ trans('settings.audit_table_related') }}</div>
+                <div class="flex-container-row flex-3">
+                    <div class="flex px-m py-xs">{{ trans('settings.audit_table_ip') }}</div>
+                    <div class="flex-2 px-m py-xs text-right">{{ trans('settings.audit_table_date') }}</div>
+                </div>
+            </div>
             @foreach($activities as $activity)
-                <tr>
-                    <td>
+                <div class="item-list-row flex-container-row items-center wrap py-xxs">
+                    <div class="flex-2 px-m py-xxs flex-container-row items-center min-width-m">
                         @include('settings.parts.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
-                    </td>
-                    <td>{{ $activity->type }}</td>
-                    <td width="40%">
+                    </div>
+                    <div class="flex-2 px-m py-xxs min-width-m"><strong class="mr-xs hide-over-m">{{ trans('settings.audit_table_event') }}:</strong> {{ $activity->type }}</div>
+                    <div class="flex-3 px-m py-xxs min-width-l">
                         @if($activity->entity)
-                            <a href="{{ $activity->entity->getUrl() }}" class="table-entity-item">
-                                <span role="presentation" class="icon text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
-                                <div class="text-{{ $activity->entity->getType() }}">
+                            <a href="{{ $activity->entity->getUrl() }}" class="flex-container-row items-center">
+                                <span role="presentation" class="icon flex-none text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
+                                <div class="flex text-{{ $activity->entity->getType() }}">
                                     {{ $activity->entity->name }}
                                 </div>
                             </a>
                         @elseif($activity->detail && $activity->isForEntity())
-                            <div class="px-m">
+                            <div>
                                 {{ trans('settings.audit_deleted_item') }} <br>
                                 {{ trans('settings.audit_deleted_item_name', ['name' => $activity->detail]) }}
                             </div>
                         @elseif($activity->detail)
-                            <div class="px-m">{{ $activity->detail }}</div>
+                            <div>{{ $activity->detail }}</div>
                         @endif
-                    </td>
-                    <td>{{ $activity->ip }}</td>
-                    <td>{{ $activity->created_at }}</td>
-                </tr>
+                    </div>
+                    <div class="flex-container-row flex-3">
+                        <div class="flex px-m py-xxs min-width-xs"><strong class="mr-xs hide-over-m">{{ trans('settings.audit_table_ip') }}:<br></strong> {{ $activity->ip }}</div>
+                        <div class="flex-2 px-m py-xxs text-m-right min-width-xs"><strong class="mr-xs hide-over-m">{{ trans('settings.audit_table_date') }}:<br></strong> {{ $activity->created_at }}</div>
+                    </div>
+                </div>
             @endforeach
-            </tbody>
-        </table>
+        </div>
 
-        {{ $activities->links() }}
+        <div class="py-m">
+            {{ $activities->links() }}
+        </div>
     </div>
 
 </div>
index a8f2777f03247cf27655fb5fbb2119409e561834..d29ad1979a0fafb95c5e8612d447a7cc89494522 100644 (file)
@@ -3,9 +3,9 @@ $user - User mode to display, Can be null.
 $user_id - Id of user to show. Must be provided.
 --}}
 @if($user)
-    <a href="{{ $user->getEditUrl() }}" class="table-user-item">
-        <div><img class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
-        <div>{{ $user->name }}</div>
+    <a href="{{ $user->getEditUrl() }}" class="flex-container-row inline gap-s items-center">
+        <div class="flex-none"><img width="40" height="40" class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
+        <div class="flex">{{ $user->name }}</div>
     </a>
 @else
     [ID: {{ $user_id }}] {{ trans('common.deleted_user') }}
index 56e2437fe0d88987776cc8d84edcbc41553ac92c..9e82ba4678d13c80d26dd6092e4307d4236e0bc4 100644 (file)
@@ -8,11 +8,11 @@
         <div class="card content-wrap auto-height">
             <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
 
-            <div class="grid half left-focus">
-                <div>
-                    <p class="text-muted">{{ trans('settings.recycle_bin_desc') }}</p>
+            <div class="flex-container-row items-center gap-x-l gap-y-m wrap">
+                <div class="flex-2 min-width-l">
+                    <p class="text-muted mb-none">{{ trans('settings.recycle_bin_desc') }}</p>
                 </div>
-                <div class="text-right">
+                <div class="flex text-m-right min-width-m">
                     <div component="dropdown" class="dropdown-container">
                         <button refs="dropdown@toggle"
                                 type="button"
                 </div>
             </div>
 
-
             <hr class="mt-l mb-s">
 
-            {!! $deletions->links() !!}
+            <div class="py-m">
+                {!! $deletions->links() !!}
+            </div>
 
-            <table class="table">
-                <tr>
-                    <th width="30%">{{ trans('settings.recycle_bin_deleted_item') }}</th>
-                    <th width="20%">{{ trans('settings.recycle_bin_deleted_parent') }}</th>
-                    <th width="20%">{{ trans('settings.recycle_bin_deleted_by') }}</th>
-                    <th width="15%">{{ trans('settings.recycle_bin_deleted_at') }}</th>
-                    <th width="15%"></th>
-                </tr>
+            <div class="item-list">
+                <div class="item-list-row flex-container-row items-center px-s bold hide-under-l">
+                    <div class="flex-2 px-m py-xs">{{ trans('settings.audit_deleted_item') }}</div>
+                    <div class="flex-2 px-m py-xs">{{ trans('settings.recycle_bin_deleted_parent') }}</div>
+                    <div class="flex-2 px-m py-xs">{{ trans('settings.recycle_bin_deleted_by') }}</div>
+                    <div class="flex px-m py-xs">{{ trans('settings.recycle_bin_deleted_at') }}</div>
+                    <div class="flex px-m py-xs text-right"></div>
+                </div>
                 @if(count($deletions) === 0)
-                    <tr>
-                        <td colspan="5">
-                            <p class="text-muted"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
-                        </td>
-                    </tr>
+                    <div class="item-list-row px-l py-m">
+                        <p class="text-muted mb-none"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
+                    </div>
                 @endif
                 @foreach($deletions as $deletion)
-                <tr>
-                    <td>
-                        <div class="table-entity-item">
-                            <span role="presentation" class="icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
-                            <div class="text-{{ $deletion->deletable->getType() }}">
-                                {{ $deletion->deletable->name }}
-                            </div>
-                        </div>
-                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
-                            <div class="mb-m"></div>
-                        @endif
-                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
-                            <div class="pl-xl block inline">
-                                <div class="text-chapter">
-                                    @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
-                                </div>
-                            </div>
-                        @endif
-                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
-                        <div class="pl-xl block inline">
-                            <div class="text-page">
-                                @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
-                            </div>
-                        </div>
-                        @endif
-                    </td>
-                    <td>
-                        @if($deletion->deletable->getParent())
-                        <div class="table-entity-item">
-                            <span role="presentation" class="icon text-{{$deletion->deletable->getParent()->getType()}}">@icon($deletion->deletable->getParent()->getType())</span>
-                            <div class="text-{{ $deletion->deletable->getParent()->getType() }}">
-                                {{ $deletion->deletable->getParent()->name }}
-                            </div>
-                        </div>
-                        @endif
-                    </td>
-                    <td>@include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
-                    <td width="200">{{ $deletion->created_at }}</td>
-                    <td width="150" class="text-right">
-                        <div component="dropdown" class="dropdown-container">
-                            <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
-                            <ul refs="dropdown@menu" class="dropdown-menu">
-                                <li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
-                                <li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
-                            </ul>
-                        </div>
-                    </td>
-                </tr>
+                    @include('settings.recycle-bin.parts.recycle-bin-list-item', ['deletion' => $deletion])
                 @endforeach
-            </table>
+            </div>
 
-            {!! $deletions->links() !!}
+            <div class="py-m">
+                {!! $deletions->links() !!}
+            </div>
 
         </div>
 
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 (file)
index 0000000..8af598b
--- /dev/null
@@ -0,0 +1,48 @@
+<div class="item-list-row flex-container-row items-center px-s wrap">
+    <div class="flex-2 px-m py-xs min-width-xl">
+        <div class="flex-container-row items-center py-xs">
+            <span role="presentation" class="flex-none icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
+            <div class="text-{{ $deletion->deletable->getType() }}">
+                {{ $deletion->deletable->name }}
+            </div>
+        </div>
+        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
+            <div class="pl-l block inline">
+                <div class="text-chapter">
+                    @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
+                </div>
+            </div>
+        @endif
+        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+            <div class="pl-l block inline">
+                <div class="text-page">
+                    @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
+                </div>
+            </div>
+        @endif
+    </div>
+    <div class="flex-2 px-m py-xs min-width-m">
+        @if($deletion->deletable->getParent())
+            <strong class="hide-over-l">{{ trans('settings.recycle_bin_deleted_parent') }}:<br></strong>
+            <div class="flex-container-row items-center">
+                <span role="presentation" class="flex-none icon text-{{$deletion->deletable->getParent()->getType()}}">@icon($deletion->deletable->getParent()->getType())</span>
+                <div class="text-{{ $deletion->deletable->getParent()->getType() }}">
+                    {{ $deletion->deletable->getParent()->name }}
+                </div>
+            </div>
+        @endif
+    </div>
+    <div class="flex-2 px-m py-xs flex-container-row items-center min-width-m">
+        <div><strong class="hide-over-l">{{ trans('settings.recycle_bin_deleted_by') }}:<br></strong>@include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</div>
+    </div>
+    <div class="flex px-m py-xs min-width-s"><strong class="hide-over-l">{{ trans('settings.recycle_bin_deleted_at') }}:<br></strong>{{ $deletion->created_at }}</div>
+    <div class="flex px-m py-xs text-m-right min-width-s">
+        <div component="dropdown" class="dropdown-container">
+            <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
+            <ul refs="dropdown@menu" class="dropdown-menu">
+                <li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+                <li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+            </ul>
+        </div>
+    </div>
+</div>
\ No newline at end of file
index 4c3b5625aa9e9ad3785218185595f33479b1973d..27ee9ce3f9022a2af5dd302e9024715410796e00 100644 (file)
                 <h1 class="list-heading">{{ trans('settings.role_user_roles') }}</h1>
 
                 <div class="text-right">
-                    <a href="{{ url("/settings/roles/new") }}" class="button outline">{{ trans('settings.role_create') }}</a>
+                    <a href="{{ url("/settings/roles/new") }}" class="button outline my-none">{{ trans('settings.role_create') }}</a>
                 </div>
             </div>
 
-            <table class="table">
-                <tr>
-                    <th>{{ trans('settings.role_name') }}</th>
-                    <th></th>
-                    <th class="text-center">{{ trans('settings.users') }}</th>
-                </tr>
+            <p class="text-muted">{{ trans('settings.roles_index_desc') }}</p>
+
+            <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap">
+                <div>
+                    <div class="block inline mr-xs">
+                        <form method="get" action="{{ url("/settings/roles") }}">
+                            <input type="text"
+                                   name="search"
+                                   placeholder="{{ trans('common.search') }}"
+                                   value="{{ $listOptions->getSearch() }}">
+                        </form>
+                    </div>
+                </div>
+                <div class="justify-flex-end">
+                    @include('common.sort', $listOptions->getSortControlData())
+                </div>
+            </div>
+
+            <div class="item-list">
                 @foreach($roles as $role)
-                    <tr>
-                        <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
-                        <td>
-                            @if($role->mfa_enforced)
-                                <span title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </span>
-                            @endif
-                            {{ $role->description }}
-                        </td>
-                        <td class="text-center">{{ $role->users->count() }}</td>
-                    </tr>
+                    @include('settings.roles.parts.roles-list-item', ['role' => $role])
                 @endforeach
-            </table>
+            </div>
 
+            <div class="mb-m">
+                {{ $roles->links() }}
+            </div>
 
         </div>
     </div>
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 (file)
index 0000000..df179a9
--- /dev/null
@@ -0,0 +1,32 @@
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="flex py-s px-m min-width-s">
+        <strong>{{ $title }}</strong> <br>
+        <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.create') }}<br></small>
+        @if($permissionPrefix === 'page' || $permissionPrefix === 'chapter')
+            @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-own', 'label' => trans('settings.role_own')])
+            <br>
+        @endif
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.view') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.edit') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.delete') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])
+    </div>
+</div>
\ No newline at end of file
index 044b4ceb47eba02fe6efd48a65548472f0cfbc87..8534b7fdbb15817b65e02fd787a8d1a2addcdceb 100644 (file)
             <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
         @endif
 
-        <table component="permissions-table" class="table toggle-switch-list compact permissions-table">
-            <tr>
-                <th width="20%">
+        <div component="permissions-table"
+             option:permissions-table:cell-selector=".item-list-row > div"
+             option:permissions-table:row-selector=".item-list-row"
+             class="item-list toggle-switch-list">
+            <div class="item-list-row flex-container-row items-center hide-under-m bold">
+                <div class="flex py-s px-m min-width-s">
                     <a href="#" refs="permissions-table@toggle-all" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.create') }}</th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.view') }}</th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.edit') }}</th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.delete') }}</th>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.shelves') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.books') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.chapters') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.pages') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.images') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
-                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}<sup>1</sup></small></td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.attachments') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
-                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.comments') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
-                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-        </table>
+                </div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.create') }}</div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.view') }}</div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.edit') }}</div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.delete') }}</div>
+            </div>
+            @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'])
+        </div>
 
         <div>
-            <p class="text-muted text-small px-m">
+            <p class="text-muted text-small p-m">
                 <sup>1</sup> {{ trans('settings.role_asset_image_view_note') }}
             </p>
         </div>
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 (file)
index 0000000..1ec3d22
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="flex py-s px-m min-width-s">
+        <strong>{{ $title }}</strong> <br>
+        <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.create') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => ''])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.view') }}<br></small>
+        <small class="faded">{{ trans('settings.role_controlled_by_asset') }}@if($refMark ?? false)<sup>{{ $refMark }}</sup>@endif</small>
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.edit') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.delete') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])
+    </div>
+</div>
\ 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 (file)
index 0000000..43e8dc8
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="item-list-row flex-container-row py-xs items-center">
+    <div class="py-xs px-m flex-2">
+        <a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a><br>
+        @if($role->mfa_enforced)
+            <small title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </small>
+        @endif
+        <small>{{ $role->description }}</small>
+    </div>
+    <div class="text-right flex py-xs px-m text-muted">
+        {{ trans_choice('settings.roles_x_users_assigned', $role->users_count, ['count' => $role->users_count]) }}
+        <br>
+        {{ trans_choice('settings.roles_x_permissions_provided', $role->permissions_count, ['count' => $role->permissions_count]) }}
+    </div>
+</div>
\ No newline at end of file
index bbe58453f38caa2d843e689d4861d994e595ff03..a564effe2b10107783a7bf24352604b428a7b598 100644 (file)
@@ -8,48 +8,48 @@
 
         <div class="card content-wrap auto-height">
 
-            <div class="grid half v-center">
+            <div class="flex-container-row items-center justify-space-between wrap">
                 <h1 class="list-heading">{{ trans('settings.webhooks') }}</h1>
 
-                <div class="text-right">
+                <div>
                     <a href="{{ url("/settings/webhooks/create") }}"
                        class="button outline">{{ trans('settings.webhooks_create') }}</a>
                 </div>
             </div>
 
-            @if(count($webhooks) > 0)
+            <p class="text-muted">{{ trans('settings.webhooks_index_desc') }}</p>
+
+            <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap">
+                <div>
+                    <div class="block inline mr-xs">
+                        <form method="get" action="{{ url("/settings/webhooks") }}">
+                            <input type="text"
+                                   name="search"
+                                   placeholder="{{ trans('common.search') }}"
+                                   value="{{ $listOptions->getSearch() }}">
+                        </form>
+                    </div>
+                </div>
+                <div class="justify-flex-end">
+                    @include('common.sort', $listOptions->getSortControlData())
+                </div>
+            </div>
 
-                <table class="table">
-                    <tr>
-                        <th>{{ trans('common.name') }}</th>
-                        <th width="100">{{ trans('settings.webhook_events_table_header') }}</th>
-                        <th width="100">{{ trans('common.status') }}</th>
-                    </tr>
+            @if(count($webhooks) > 0)
+                <div class="item-list">
                     @foreach($webhooks as $webhook)
-                        <tr>
-                            <td>
-                                <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> <br>
-                                <span class="small text-muted italic">{{ $webhook->endpoint }}</span>
-                            </td>
-                            <td>
-                                @if($webhook->tracksEvent('all'))
-                                    {{ trans('settings.webhooks_events_all') }}
-                                @else
-                                    {{ $webhook->trackedEvents->count() }}
-                                @endif
-                            </td>
-                            <td>
-                                {{ trans('common.status_' . ($webhook->active ? 'active' : 'inactive')) }}
-                            </td>
-                        </tr>
+                        @include('settings.webhooks.parts.webhooks-list-item', ['webhook' => $webhook])
                     @endforeach
-                </table>
+                </div>
             @else
                 <p class="text-muted empty-text px-none">
                     {{ trans('settings.webhooks_none_created') }}
                 </p>
             @endif
 
+            <div class="my-m">
+                {{ $webhooks->links() }}
+            </div>
 
         </div>
     </div>
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 (file)
index 0000000..0ba6131
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="item-list-row py-s">
+    <div class="flex-container-row">
+        <div class="flex-2 px-m flex-container-row items-center gap-xs">
+            @include('common.status-indicator', ['status' => $webhook->active])
+            <div>&nbsp;<a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a></div>
+        </div>
+        <div class="flex px-m text-right text-muted">
+            @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
+        </div>
+    </div>
+    <div class="px-m text-muted italic text-limit-lines-1">
+        <small>{{ $webhook->endpoint }}</small>
+    </div>
+</div>
\ No newline at end of file
index ee52769aa0dc11ab0b23fc9aefdab43ea8bc665e..75d46318f019a3f04827f196cc63e61349bda0bc 100644 (file)
@@ -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')
index d78606ac700e081818d7c183e880116692a6b938..da9c06d9208a585224e823e3125fa0ad3c8759d5 100644 (file)
@@ -1,10 +1,9 @@
-
 <main class="content-wrap mt-m card">
 
     <div class="grid half v-center">
         <h1 class="list-heading">{{ trans('entities.shelves') }}</h1>
         <div class="text-right">
-            @include('entities.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'bookshelves'])
+            @include('common.sort', $listOptions->getSortControlData())
         </div>
     </div>
 
@@ -31,7 +30,8 @@
     @else
         <p class="text-muted">{{ trans('entities.shelves_empty') }}</p>
         @if(userCan('bookshelf-create-all'))
-            <a href="{{ url("/create-shelf") }}" class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
+            <a href="{{ url("/create-shelf") }}"
+               class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
         @endif
     @endif
 
index 37d2889563063be922a8683d60ee655787bcf7df..0195759d818592ee421cac2ba46b1c22cad5c0c1 100644 (file)
             <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('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())
             </div>
         </div>
 
index c88449ce7ad1e9492bb4b303cc683bfbe76f0d75..b6b3325e0516a15d3c9ab6aabd4ebad5afcdd739 100644 (file)
@@ -5,25 +5,28 @@
 
         <main class="card content-wrap mt-xxl">
 
-            <div class="flex-container-row wrap justify-space-between items-center mb-s">
-                <h1 class="list-heading">{{ trans('entities.tags') }}</h1>
-
-                <div>
-                    <div class="block inline mr-xs">
-                        <form method="get" action="{{ url("/tags") }}">
-                            @include('form.request-query-inputs', ['params' => ['name']])
-                            <input type="text"
-                                   name="search"
-                                   placeholder="{{ trans('common.search') }}"
-                                   value="{{ $search }}">
-                        </form>
-                    </div>
+            <h1 class="list-heading">{{ trans('entities.tags') }}</h1>
+
+            <p class="text-muted">{{ trans('entities.tags_index_desc') }}</p>
+
+            <div class="flex-container-row wrap justify-space-between items-center mb-s gap-m">
+                <div class="block inline mr-xs">
+                    <form method="get" action="{{ url("/tags") }}">
+                        @include('form.request-query-inputs', ['params' => ['name']])
+                        <input type="text"
+                               name="search"
+                               placeholder="{{ trans('common.search') }}"
+                               value="{{ $listOptions->getSearch() }}">
+                    </form>
+                </div>
+                <div class="block inline">
+                    @include('common.sort', $listOptions->getSortControlData())
                 </div>
             </div>
 
             @if($nameFilter)
-                <div class="mb-m">
-                    <span class="mr-xs">{{ trans('common.filter_active') }}</span>
+                <div class="my-m">
+                    <strong class="mr-xs">{{ trans('common.filter_active') }}</strong>
                     @include('entities.tag', ['tag' => new \BookStack\Actions\Tag(['name' => $nameFilter])])
                     <form method="get" action="{{ url("/tags") }}" class="inline block">
                         @include('form.request-query-inputs', ['params' => ['search']])
             @endif
 
             @if(count($tags) > 0)
-                <table class="table expand-to-padding mt-m">
+                <div class="item-list mt-m">
                     @foreach($tags as $tag)
-                        @include('tags.parts.table-row', ['tag' => $tag, 'nameFilter' => $nameFilter])
+                        @include('tags.parts.tags-list-item', ['tag' => $tag, 'nameFilter' => $nameFilter])
                     @endforeach
-                </table>
+                </div>
 
-                <div>
+                <div class="my-m">
                     {{ $tags->links() }}
                 </div>
             @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 (file)
index aa04959..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<tr>
-    <td>
-        <span class="text-bigger mr-xl">@include('entities.tag', ['tag' => $tag])</span>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() }}"
-           title="{{ trans('entities.tags_usages') }}"
-           class="pill text-muted">@icon('leaderboard'){{ $tag->usages }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:page}' }}"
-           title="{{ trans('entities.tags_assigned_pages') }}"
-           class="pill text-page">@icon('page'){{ $tag->page_count }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:chapter}' }}"
-           title="{{ trans('entities.tags_assigned_chapters') }}"
-           class="pill text-chapter">@icon('chapter'){{ $tag->chapter_count }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:book}' }}"
-           title="{{ trans('entities.tags_assigned_books') }}"
-           class="pill text-book">@icon('book'){{ $tag->book_count }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:bookshelf}' }}"
-           title="{{ trans('entities.tags_assigned_shelves') }}"
-           class="pill text-bookshelf">@icon('bookshelf'){{ $tag->shelf_count }}</a>
-    </td>
-    <td class="text-right text-muted">
-        @if($tag->values ?? false)
-            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }}</a>
-        @elseif(empty($nameFilter))
-            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_all_values') }}</a>
-        @endif
-    </td>
-</tr>
\ 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 (file)
index 0000000..3962db7
--- /dev/null
@@ -0,0 +1,31 @@
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="{{ isset($nameFilter) && $tag->value ? 'flex-2' : 'flex' }} py-s px-m min-width-m">
+        <span class="text-bigger mr-xl">@include('entities.tag', ['tag' => $tag])</span>
+    </div>
+    <div class="flex-2 flex-container-row justify-center items-center gap-m py-s px-m min-width-l wrap">
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() }}"
+           title="{{ trans('entities.tags_usages') }}"
+           class="flex fill-area min-width-xxs bold text-right text-muted"><span class="opacity-60">@icon('leaderboard')</span>{{ $tag->usages }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:page}' }}"
+           title="{{ trans('entities.tags_assigned_pages') }}"
+           class="flex fill-area min-width-xxs bold text-right text-page"><span class="opacity-60">@icon('page')</span>{{ $tag->page_count }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:chapter}' }}"
+           title="{{ trans('entities.tags_assigned_chapters') }}"
+           class="flex fill-area min-width-xxs bold text-right text-chapter"><span class="opacity-60">@icon('chapter')</span>{{ $tag->chapter_count }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:book}' }}"
+           title="{{ trans('entities.tags_assigned_books') }}"
+           class="flex fill-area min-width-xxs bold text-right text-book"><span class="opacity-60">@icon('book')</span>{{ $tag->book_count }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:bookshelf}' }}"
+           title="{{ trans('entities.tags_assigned_shelves') }}"
+           class="flex fill-area min-width-xxs bold text-right text-bookshelf"><span class="opacity-60">@icon('bookshelf')</span>{{ $tag->shelf_count }}</a>
+    </div>
+    @if($tag->values ?? false)
+        <div class="flex text-s-right text-muted py-s px-m min-width-s">
+            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }}</a>
+        </div>
+    @elseif(empty($nameFilter))
+        <div class="flex text-s-right text-muted py-s px-m min-width-s">
+            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_all_values') }}</a>
+        </div>
+    @endif
+</div>
\ No newline at end of file
index ea18933727b03272bf5bf2af710fefc56129b5c0..58617fb85d2f5871e8ce5f3205b69d5703624387 100644 (file)
@@ -1,6 +1,6 @@
 <section class="card content-wrap auto-height" id="api_tokens">
-    <div class="grid half mb-s">
-        <div><h2 class="list-heading">{{ trans('settings.users_api_tokens') }}</h2></div>
+    <div class="flex-container-row wrap justify-space-between items-center mb-s">
+        <h2 class="list-heading">{{ trans('settings.users_api_tokens') }}</h2>
         <div class="text-right pt-xs">
             @if(userCan('access-api'))
                 <a href="{{ url('/api/docs') }}" class="button outline">{{ trans('settings.users_api_tokens_docs') }}</a>
@@ -9,25 +9,25 @@
         </div>
     </div>
     @if (count($user->apiTokens) > 0)
-        <table class="table">
-            <tr>
-                <th>{{ trans('common.name') }}</th>
-                <th>{{ trans('settings.users_api_tokens_expires') }}</th>
-                <th></th>
-            </tr>
+        <div class="item-list my-m">
             @foreach($user->apiTokens as $token)
-                <tr>
-                    <td>
-                        {{ $token->name }} <br>
+                <div class="item-list-row flex-container-row items-center wrap py-xs gap-x-m">
+                    <div class="flex px-m py-xs min-width-m">
+                        <a href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ $token->name }}</a> <br>
                         <span class="small text-muted italic">{{ $token->token_id }}</span>
-                    </td>
-                    <td>{{ $token->expires_at->format('Y-m-d') ?? '' }}</td>
-                    <td class="text-right">
-                        <a class="button outline small" href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ trans('common.edit') }}</a>
-                    </td>
-                </tr>
+                    </div>
+                    <div class="flex flex-container-row items-center min-width-m">
+                        <div class="flex px-m py-xs text-muted">
+                            <strong class="text-small">{{ trans('settings.users_api_tokens_expires') }}</strong> <br>
+                            {{ $token->expires_at->format('Y-m-d') ?? '' }}
+                        </div>
+                        <div class="flex px-m py-xs text-right">
+                            <a class="button outline small" href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ trans('common.edit') }}</a>
+                        </div>
+                    </div>
+                </div>
             @endforeach
-        </table>
+        </div>
     @else
         <p class="text-muted italic py-m">{{ trans('settings.users_api_tokens_none') }}</p>
     @endif
index 03eae2c005a660e3e61edd6d1d236a7b5723df8b..0dd607f8c7de26f39608d113536df246afa3be22 100644 (file)
@@ -9,59 +9,34 @@
 
             <div class="flex-container-row wrap justify-space-between items-center">
                 <h1 class="list-heading">{{ trans('settings.users') }}</h1>
+                <div>
+                    <a href="{{ url("/settings/users/create") }}" class="outline button my-none">{{ trans('settings.users_add_new') }}</a>
+                </div>
+            </div>
 
+            <p class="text-muted">{{ trans('settings.users_index_desc') }}</p>
+
+            <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap">
                 <div>
                     <div class="block inline mr-xs">
                         <form method="get" action="{{ url("/settings/users") }}">
-                            @foreach(collect($listDetails)->except('search') as $name => $val)
-                                <input type="hidden" name="{{ $name }}" value="{{ $val }}">
-                            @endforeach
-                            <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>
-                    <a href="{{ url("/settings/users/create") }}" class="outline button mt-none">{{ trans('settings.users_add_new') }}</a>
+                </div>
+                <div class="justify-flex-end">
+                    @include('common.sort', $listOptions->getSortControlData())
                 </div>
             </div>
 
-            <table class="table">
-                <tr>
-                    <th width="9%"></th>
-                    <th width="36%">
-                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'name']) }}">{{ trans('auth.name') }}</a>
-                        /
-                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">{{ trans('auth.email') }}</a>
-                    </th>
-                    <th width="35%">{{ trans('settings.role_user_roles') }}</th>
-                    <th class="text-right" width="20%">
-                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'last_activity_at']) }}">{{ trans('settings.users_latest_activity') }}</a>
-                    </th>
-                </tr>
+            <div class="item-list">
                 @foreach($users as $user)
-                    <tr>
-                        <td class="text-center" style="line-height: 0;"><img class="avatar med" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></td>
-                        <td>
-                            <a href="{{ url("/settings/users/{$user->id}") }}">
-                                {{ $user->name }}
-                                <br>
-                                <span class="text-muted">{{ $user->email }}</span>
-                                @if($user->mfa_values_count > 0)
-                                    <span title="MFA Configured" class="text-pos">@icon('lock')</span>
-                                @endif
-                            </a>
-                        </td>
-                        <td>
-                            @foreach($user->roles as $index => $role)
-                                <small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
-                            @endforeach
-                        </td>
-                        <td class="text-right text-muted">
-                            @if($user->last_activity_at)
-                                <small title="{{ $user->last_activity_at->format('Y-m-d H:i:s') }}">{{ $user->last_activity_at->diffForHumans() }}</small>
-                            @endif
-                        </td>
-                    </tr>
+                    @include('users.parts.users-list-item', ['user' => $user])
                 @endforeach
-            </table>
+            </div>
 
             <div>
                 {{ $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 (file)
index 0000000..dc7c9f2
--- /dev/null
@@ -0,0 +1,27 @@
+<div class="flex-container-row item-list-row items-center wrap py-xs">
+    <div class="px-m py-xs flex-container-row items-center flex-2 gap-l min-width-m">
+        <img class="avatar med" width="40" height="40" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}">
+        <a href="{{ url("/settings/users/{$user->id}") }}">
+            {{ $user->name }}
+            <br>
+            <span class="text-muted">{{ $user->email }}</span>
+            @if($user->mfa_values_count > 0)
+                <span title="MFA Configured" class="text-pos">@icon('lock')</span>
+            @endif
+        </a>
+    </div>
+    <div class="flex-container-row items-center flex-3 min-width-m">
+        <div class="px-m py-xs flex">
+            @foreach($user->roles as $index => $role)
+                <small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
+            @endforeach
+        </div>
+        <div class="px-m py-xs flex text-right text-muted">
+            @if($user->last_activity_at)
+                <small>{{ trans('settings.users_latest_activity') }}</small>
+                <br>
+                <small title="{{ $user->last_activity_at->format('Y-m-d H:i:s') }}">{{ $user->last_activity_at->diffForHumans() }}</small>
+            @endif
+        </div>
+    </div>
+</div>
\ No newline at end of file
index 1cffbfd7d8d20c7d5779d30a69980e0fc760fe2f..b3f11f53a7802a9fe3b522d532f2a337d2974b37 100644 (file)
@@ -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']);
index 987e23a45bd42f917ceb2c741168021e6b5c5b51..25fa2b7963893a5cbb3689a1b8e7698c817f731a 100644 (file)
@@ -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()
index d00ec5ce59019e8962b49955732c2a4de919b28a..0749888c833d698ec7e3efe11ac8fb9c05007f32 100644 (file)
@@ -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()
index ed5c798a5f614be01dbffc714f23ec2badf71dac..ab06686e0e6f3d5abb7718fe79efd614842667e8 100644 (file)
@@ -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');
     }
 
index 3d27e9c8d48c3cca7e41a3f1e87cf0b9e8609f86..990df607e1c14b9f5c37cb6c1f2a488ef77e950b 100644 (file)
@@ -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()
index c65b11d7d896a712a732dea8e6831c55f1b39ad2..92e4158cd955d6a7623445e488c0e3d5ea82e8fc 100644 (file)
@@ -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();