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;
/**
* 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',
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);
namespace BookStack\Http\Controllers;
use BookStack\Actions\TagRepo;
+use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
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,
+ ...$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,
]);
}
*/
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)) {
return redirect()->back(500);
}
'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.",
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
*/
flex-basis: auto;
flex-grow: 0;
}
+ &.fill-area {
+ flex-grow: 1;
+ flex-shrink: 0;
+ min-width: fit-content;
+ }
}
.flex-2 {
--- /dev/null
+
+.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
@import "variables";
@import "mixins";
@import "spacing";
+@import "opacity";
@import "html";
@import "text";
@import "colors";
<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
+++ /dev/null
-<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
--- /dev/null
+<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