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