$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,
'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.',
<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') }}"
</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>
--- /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
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;
$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');
+ }
}
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.
*