Updated tags list to new responsive format

This commit is contained in:
Dan Brown
2022-10-31 11:40:28 +00:00
parent 9e8516c2df
commit 80d2889217
11 changed files with 109 additions and 90 deletions

View File

@ -4,6 +4,7 @@ namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Util\SimpleListOptions;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -20,8 +21,14 @@ class TagRepo
/** /**
* Start a query against all tags in the system. * 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() $query = Tag::query()
->select([ ->select([
'name', '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 = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'), DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
]) ])
->orderBy($nameFilter ? 'value' : 'name'); ->orderBy($sort, $listOptions->getOrder());
if ($nameFilter) { if ($nameFilter) {
$query->where('name', '=', $nameFilter); $query->where('name', '=', $nameFilter);

View File

@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use BookStack\Actions\TagRepo; use BookStack\Actions\TagRepo;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class TagController extends Controller class TagController extends Controller
@ -19,22 +20,26 @@ class TagController extends Controller
*/ */
public function index(Request $request) 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', ''); $nameFilter = $request->get('name', '');
$tags = $this->tagRepo $tags = $this->tagRepo
->queryWithTotals($search, $nameFilter) ->queryWithTotals($listOptions, $nameFilter)
->paginate(50) ->paginate(50)
->appends(array_filter([ ->appends(array_filter([
'search' => $search, ...$listOptions->getPaginationAppends(),
'name' => $nameFilter, 'name' => $nameFilter,
])); ]));
$this->setPageTitle(trans('entities.tags')); $this->setPageTitle(trans('entities.tags'));
return view('tags.index', [ return view('tags.index', [
'tags' => $tags, 'tags' => $tags,
'search' => $search, 'nameFilter' => $nameFilter,
'nameFilter' => $nameFilter, 'listOptions' => $listOptions,
]); ]);
} }

View File

@ -62,7 +62,7 @@ class UserPreferencesController extends Controller
*/ */
public function changeSort(Request $request, string $id, string $type) public function changeSort(Request $request, string $id, string $type)
{ {
$validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks']; $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags'];
if (!in_array($type, $validSortTypes)) { if (!in_array($type, $validSortTypes)) {
return redirect()->back(500); return redirect()->back(500);
} }

View File

@ -275,6 +275,7 @@ return [
'shelf_tags' => 'Shelf Tags', 'shelf_tags' => 'Shelf Tags',
'tag' => 'Tag', 'tag' => 'Tag',
'tags' => 'Tags', '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_name' => 'Tag Name',
'tag_value' => 'Tag Value (Optional)', '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.", 'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",

View File

@ -286,35 +286,10 @@
margin-bottom: 0; margin-bottom: 0;
} }
td .tag-item { .item-list-row .tag-item {
margin-bottom: 0; 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 * API Docs
*/ */

View File

@ -160,6 +160,11 @@ body.flexbox {
flex-basis: auto; flex-basis: auto;
flex-grow: 0; flex-grow: 0;
} }
&.fill-area {
flex-grow: 1;
flex-shrink: 0;
min-width: fit-content;
}
} }
.flex-2 { .flex-2 {

View File

@ -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;
}

View File

@ -4,6 +4,7 @@
@import "variables"; @import "variables";
@import "mixins"; @import "mixins";
@import "spacing"; @import "spacing";
@import "opacity";
@import "html"; @import "html";
@import "text"; @import "text";
@import "colors"; @import "colors";

View File

@ -5,25 +5,28 @@
<main class="card content-wrap mt-xxl"> <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>
<h1 class="list-heading">{{ trans('entities.tags') }}</h1>
<div> <p class="text-muted">{{ trans('entities.tags_index_desc') }}</p>
<div class="block inline mr-xs">
<form method="get" action="{{ url("/tags") }}"> <div class="flex-container-row wrap justify-space-between items-center mb-s gap-m">
@include('form.request-query-inputs', ['params' => ['name']]) <div class="block inline mr-xs">
<input type="text" <form method="get" action="{{ url("/tags") }}">
name="search" @include('form.request-query-inputs', ['params' => ['name']])
placeholder="{{ trans('common.search') }}" <input type="text"
value="{{ $search }}"> name="search"
</form> placeholder="{{ trans('common.search') }}"
</div> value="{{ $listOptions->getSearch() }}">
</form>
</div>
<div class="block inline">
@include('common.sort', $listOptions->getSortControlData())
</div> </div>
</div> </div>
@if($nameFilter) @if($nameFilter)
<div class="mb-m"> <div class="my-m">
<span class="mr-xs">{{ trans('common.filter_active') }}</span> <strong class="mr-xs">{{ trans('common.filter_active') }}</strong>
@include('entities.tag', ['tag' => new \BookStack\Actions\Tag(['name' => $nameFilter])]) @include('entities.tag', ['tag' => new \BookStack\Actions\Tag(['name' => $nameFilter])])
<form method="get" action="{{ url("/tags") }}" class="inline block"> <form method="get" action="{{ url("/tags") }}" class="inline block">
@include('form.request-query-inputs', ['params' => ['search']]) @include('form.request-query-inputs', ['params' => ['search']])
@ -33,13 +36,13 @@
@endif @endif
@if(count($tags) > 0) @if(count($tags) > 0)
<table class="table expand-to-padding mt-m"> <div class="item-list mt-m">
@foreach($tags as $tag) @foreach($tags as $tag)
@include('tags.parts.table-row', ['tag' => $tag, 'nameFilter' => $nameFilter]) @include('tags.parts.tags-list-item', ['tag' => $tag, 'nameFilter' => $nameFilter])
@endforeach @endforeach
</table> </div>
<div> <div class="my-m">
{{ $tags->links() }} {{ $tags->links() }}
</div> </div>
@else @else

View File

@ -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>

View File

@ -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>