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 @@
- @include('form.request-query-inputs', ['params' => ['page', 'name']]) + @include('form.request-query-inputs', ['params' => ['name']]) @endif + @if(count($tags) > 0) + + @foreach($tags as $tag) + @include('tags.parts.table-row', ['tag' => $tag, 'nameFilter' => $nameFilter]) + @endforeach +
- - @foreach($tags as $tag) - - - - - - - - - - @endforeach -
- @include('entities.tag', ['tag' => $tag]) - - @icon('leaderboard'){{ $tag->usages }} - - @icon('page'){{ $tag->page_count }} - - @icon('chapter'){{ $tag->chapter_count }} - - @icon('book'){{ $tag->book_count }} - - @icon('bookshelf'){{ $tag->shelf_count }} - - @if($tag->values ?? false) - {{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }} - @elseif(empty($nameFilter)) - {{ trans('entities.tags_all_values') }} - @endif -
- -
- {{ $tags->links() }} -
+
+ {{ $tags->links() }} +
+ @else +

+ {{ trans('common.no_items') }}. +
+ {{ trans('entities.tags_list_empty_hint') }} +

+ @endif
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 @@ + + + @include('entities.tag', ['tag' => $tag]) + + + @icon('leaderboard'){{ $tag->usages }} + + + @icon('page'){{ $tag->page_count }} + + + @icon('chapter'){{ $tag->chapter_count }} + + + @icon('book'){{ $tag->book_count }} + + + @icon('bookshelf'){{ $tag->shelf_count }} + + + @if($tag->values ?? false) + {{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }} + @elseif(empty($nameFilter)) + {{ trans('entities.tags_all_values') }} + @endif + + \ No newline at end of file diff --git a/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. *