diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index c8292a16b..aaf0cb9b2 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -26,7 +26,11 @@ class TagController extends Controller $nameFilter = $request->get('name', ''); $tags = $this->tagRepo ->queryWithTotals($search, $nameFilter) - ->paginate(20); + ->paginate(50) + ->appends(array_filter([ + 'search' => $search, + 'name' => $nameFilter + ])); return view('tags.index', [ 'tags' => $tags, diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 1244fe82a..5cf47629a 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -267,6 +267,7 @@ return [ 'tags_all_values' => 'All values', 'tags_view_tags' => 'View Tags', 'tags_view_existing_tags' => 'View existing tags', + 'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.', 'attachments' => 'Attachments', 'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.', 'attachments_explain_instant_save' => 'Changes here are saved instantly.', diff --git a/resources/views/tags/index.blade.php b/resources/views/tags/index.blade.php index de231493e..c88449ce7 100644 --- a/resources/views/tags/index.blade.php +++ b/resources/views/tags/index.blade.php @@ -11,7 +11,7 @@ <div> <div class="block inline mr-xs"> <form method="get" action="{{ url("/tags") }}"> - @include('form.request-query-inputs', ['params' => ['page', 'name']]) + @include('form.request-query-inputs', ['params' => ['name']]) <input type="text" name="search" placeholder="{{ trans('common.search') }}" @@ -32,52 +32,23 @@ </div> @endif + @if(count($tags) > 0) + <table class="table expand-to-padding mt-m"> + @foreach($tags as $tag) + @include('tags.parts.table-row', ['tag' => $tag, 'nameFilter' => $nameFilter]) + @endforeach + </table> - <table class="table expand-to-padding mt-m"> - @foreach($tags as $tag) - <tr> - <td> - <span class="text-bigger mr-xl">@include('entities.tag', ['tag' => $tag])</span> - </td> - <td width="60" 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="60" 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="60" 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="60" 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="60" 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> - @endforeach - </table> - - <div> - {{ $tags->links() }} - </div> + <div> + {{ $tags->links() }} + </div> + @else + <p class="text-muted italic my-xl"> + {{ trans('common.no_items') }}. + <br> + {{ trans('entities.tags_list_empty_hint') }} + </p> + @endif </main> </div> diff --git a/resources/views/tags/parts/table-row.blade.php b/resources/views/tags/parts/table-row.blade.php new file mode 100644 index 000000000..aa04959a9 --- /dev/null +++ b/resources/views/tags/parts/table-row.blade.php @@ -0,0 +1,37 @@ +<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/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index 9b3fb1532..db76cae5f 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -3,6 +3,7 @@ namespace Tests\Entity; use BookStack\Actions\Tag; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use Tests\TestCase; @@ -98,4 +99,95 @@ class TagTest extends TestCase $resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'color'); $resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'red'); } + + public function test_tags_index_shows_tag_name_as_expected_with_right_counts() + { + /** @var Page $page */ + $page = Page::query()->first(); + $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); + $page->tags()->create(['name' => 'Category', 'value' => 'OtherTestContent']); + + $resp = $this->asEditor()->get('/tags'); + $resp->assertSee('Category'); + $resp->assertElementCount('.tag-item', 1); + $resp->assertDontSee('GreatTestContent'); + $resp->assertDontSee('OtherTestContent'); + $resp->assertElementContains('a[title="Total tag usages"]', '2'); + $resp->assertElementContains('a[title="Assigned to Pages"]', '2'); + $resp->assertElementContains('a[title="Assigned to Books"]', '0'); + $resp->assertElementContains('a[title="Assigned to Chapters"]', '0'); + $resp->assertElementContains('a[title="Assigned to Shelves"]', '0'); + $resp->assertElementContains('a[href$="/tags?name=Category"]', '2 unique values'); + + /** @var Book $book */ + $book = Book::query()->first(); + $book->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); + $resp = $this->asEditor()->get('/tags'); + $resp->assertElementContains('a[title="Total tag usages"]', '3'); + $resp->assertElementContains('a[title="Assigned to Books"]', '1'); + $resp->assertElementContains('a[href$="/tags?name=Category"]', '2 unique values'); + } + + public function test_tag_index_can_be_searched() + { + /** @var Page $page */ + $page = Page::query()->first(); + $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); + + $resp = $this->asEditor()->get('/tags?search=cat'); + $resp->assertElementContains('.tag-item .tag-name', 'Category'); + + $resp = $this->asEditor()->get('/tags?search=content'); + $resp->assertElementContains('.tag-item .tag-name', 'Category'); + $resp->assertElementContains('.tag-item .tag-value', 'GreatTestContent'); + + $resp = $this->asEditor()->get('/tags?search=other'); + $resp->assertElementNotExists('.tag-item .tag-name'); + } + + public function test_tag_index_can_be_scoped_to_specific_tag_name() + { + /** @var Page $page */ + $page = Page::query()->first(); + $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); + $page->tags()->create(['name' => 'Category', 'value' => 'OtherTestContent']); + $page->tags()->create(['name' => 'OtherTagName', 'value' => 'OtherValue']); + + $resp = $this->asEditor()->get('/tags?name=Category'); + $resp->assertSee('Category'); + $resp->assertSee('GreatTestContent'); + $resp->assertSee('OtherTestContent'); + $resp->assertDontSee('OtherTagName'); + $resp->assertElementCount('table .tag-item', 2); + $resp->assertSee('Active Filter:'); + $resp->assertElementContains('form[action$="/tags"]', 'Clear Filter'); + } + + public function test_tags_index_adheres_to_page_permissions() + { + /** @var Page $page */ + $page = Page::query()->first(); + $page->tags()->create(['name' => 'SuperCategory', 'value' => 'GreatTestContent']); + + $resp = $this->asEditor()->get('/tags'); + $resp->assertSee('SuperCategory'); + $resp = $this->get('/tags?name=SuperCategory'); + $resp->assertSee('GreatTestContent'); + + $page->restricted = true; + $this->regenEntityPermissions($page); + + $resp = $this->asEditor()->get('/tags'); + $resp->assertDontSee('SuperCategory'); + $resp = $this->get('/tags?name=SuperCategory'); + $resp->assertDontSee('GreatTestContent'); + } + + public function test_tag_index_shows_message_on_no_results() + { + /** @var Page $page */ + $resp = $this->asEditor()->get('/tags?search=testingval'); + $resp->assertSee('No items available'); + $resp->assertSee('Tags can be assigned via the page editor sidebar'); + } } diff --git a/tests/TestResponse.php b/tests/TestResponse.php index 5e2be3ac3..4e53aa020 100644 --- a/tests/TestResponse.php +++ b/tests/TestResponse.php @@ -53,6 +53,26 @@ class TestResponse extends BaseTestResponse return $this; } + /** + * Assert the response contains the given count of elements + * that match the given css selector. + * + * @return $this + */ + public function assertElementCount(string $selector, int $count) + { + $elements = $this->crawler()->filter($selector); + PHPUnit::assertTrue( + $elements->count() === $count, + 'Unable to ' . $count . ' element(s) matching the selector: ' . PHP_EOL . PHP_EOL . + "[{$selector}]" . PHP_EOL . PHP_EOL . + 'found ' . $elements->count() . ' within' . PHP_EOL . PHP_EOL . + "[{$this->getContent()}]." + ); + + return $this; + } + /** * Assert the response does not contain the specified element. *