Added bookshelves to breadcrumbs

- Updated breadcrumb dropdown switchers and back-end sibling code to handle new breadcrumbs.
- Added breadcrumb view composer and EntityContext system to mangage
tracking if in the context of a bookshelf.
This commit is contained in:
Dan Brown 2019-04-07 18:28:11 +01:00
parent 221a483b40
commit b12ae6d11b
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 202 additions and 29 deletions

View File

@ -92,4 +92,14 @@ class Bookshelf extends Entity
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
/**
* Check if this shelf contains the given book.
* @param Book $book
* @return bool
*/
public function contains(Book $book)
{
return $this->books()->where('id', '=', $book->id)->count() > 0;
}
}

View File

@ -0,0 +1,34 @@
<?php namespace BookStack\Entities;
use Illuminate\View\View;
class BreadcrumbsViewComposer
{
protected $entityContextManager;
/**
* BreadcrumbsViewComposer constructor.
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityContextManager $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
/**
* Modify data when the view is composed.
* @param View $view
*/
public function compose(View $view)
{
$crumbs = $view->getData()['crumbs'];
if (array_first($crumbs) instanceof Book) {
$shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
if ($shelf) {
array_unshift($crumbs, $shelf);
$view->with('crumbs', $crumbs);
}
}
}
}

View File

@ -0,0 +1,62 @@
<?php namespace BookStack\Entities;
use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Session\Store;
class EntityContextManager
{
protected $session;
protected $entityRepo;
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
/**
* EntityContextManager constructor.
* @param Store $session
* @param EntityRepo $entityRepo
*/
public function __construct(Store $session, EntityRepo $entityRepo)
{
$this->session = $session;
$this->entityRepo = $entityRepo;
}
/**
* Get the current bookshelf context for the given book.
* @param Book $book
* @return Bookshelf|null
*/
public function getContextualShelfForBook(Book $book)
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
if (is_int($contextBookshelfId)) {
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
if ($shelf && $shelf->contains($book)) {
return $shelf;
}
}
return null;
}
/**
* Store the current contextual shelf ID.
* @param int $shelfId
*/
public function setShelfContext(int $shelfId)
{
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
}
/**
* Clear the session stored shelf context id.
*/
public function clearShelfContext()
{
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
}
}

View File

@ -3,6 +3,7 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Book;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use Illuminate\Http\Request;
@ -15,18 +16,25 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param \BookStack\Auth\UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param UserRepo $userRepo
* @param ExportService $exportService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
public function __construct(
EntityRepo $entityRepo,
UserRepo $userRepo,
ExportService $exportService,
EntityContextManager $entityContextManager
) {
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
@ -50,6 +58,8 @@ class BookController extends Controller
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.books'));
return view('books.index', [
'books' => $books,
@ -95,14 +105,22 @@ class BookController extends Controller
/**
* Display the specified book.
* @param $slug
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show($slug)
public function show($slug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}
$this->setPageTitle($book->getShortName());
return view('books.show', [
'book' => $book,

View File

@ -3,6 +3,7 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -13,16 +14,19 @@ class BookshelfController extends Controller
protected $entityRepo;
protected $userRepo;
protected $entityContextManager;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
@ -32,9 +36,7 @@ class BookshelfController extends Controller
*/
public function index()
{
$view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
$order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
$sortOptions = [
@ -43,14 +45,16 @@ class BookshelfController extends Controller
'updated_at' => trans('common.sort_updated_at'),
];
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order, function($query) {
$query->with(['books']);
});
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
foreach ($shelves as $shelf) {
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
}
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
return view('shelves.index', [
'shelves' => $shelves,
@ -105,11 +109,13 @@ class BookshelfController extends Controller
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
/** @var Bookshelf $bookshelf */
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->entityContextManager->setShelfContext($bookshelf->id);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves.show', [

View File

@ -1,35 +1,45 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\SearchService;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
protected $entityContextManager;
/**
* SearchController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
public function __construct(
EntityRepo $entityRepo,
ViewService $viewService,
SearchService $searchService,
EntityContextManager $entityContextManager
) {
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
/**
* Searches all entities.
* @param Request $request
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function search(Request $request)
@ -56,7 +66,7 @@ class SearchController extends Controller
* Searches all entities within a book.
* @param Request $request
* @param integer $bookId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchBook(Request $request, $bookId)
@ -70,7 +80,7 @@ class SearchController extends Controller
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
@ -106,7 +116,7 @@ class SearchController extends Controller
/**
* Search siblings items in the system.
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|mixed
* @return Factory|View|mixed
*/
public function searchSiblings(Request $request)
{
@ -130,16 +140,21 @@ class SearchController extends Controller
$entities = $this->entityRepo->getBookDirectChildren($entity->book);
}
// Book in shelf
// TODO - When shelve tracking added, Update below if criteria
// Book
// Gets just the books in a shelf if shelf is in context
if ($entity->isA('book')) {
$entities = $this->entityRepo->getAll('book');
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
if ($contextShelf) {
$entities = $this->entityRepo->getBookshelfChildren($contextShelf);
} else {
$entities = $this->entityRepo->getAll('book');
}
}
// Shelve
// TODO - When shelve tracking added
if ($entity->isA('bookshelf')) {
$entities = $this->entityRepo->getAll('bookshelf');
}
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}

View File

@ -3,12 +3,14 @@
use Blade;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Schema;
use Validator;
@ -33,7 +35,6 @@ class AppServiceProvider extends ServiceProvider
return substr_count($uploadName, '.') < 2;
});
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";
@ -49,6 +50,9 @@ class AppServiceProvider extends ServiceProvider
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
]);
// View Composers
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
}
/**

View File

@ -25,10 +25,8 @@ class BreadcrumbListing {
onSearch() {
const input = this.searchInput.value.toLowerCase().trim();
const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
console.log(listItems);
for (let listItem of listItems) {
const match = !input || listItem.textContent.toLowerCase().includes(input);
console.log(match);
listItem.style.display = match ? 'flex' : 'none';
}
}

View File

@ -12,7 +12,7 @@
<div class="entity-shelf-books grid third gap-y-xs entity-list-item-children">
@foreach($shelf->books as $book)
<div>
<a href="{{ $book->getUrl() }}" class="entity-chip text-book">
<a href="{{ $book->getUrl('?shelf=' . $shelf->id) }}" class="entity-chip text-book">
@icon('book')
{{ $book->name }}
</a>

View File

@ -185,4 +185,30 @@ class BookShelfTest extends TestCase
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
}
public function test_bookshelves_show_in_breadcrumbs_if_in_context()
{
$shelf = Bookshelf::first();
$shelfBook = $shelf->books()->first();
$shelfPage = $shelfBook->pages()->first();
$this->asAdmin();
$bookVisit = $this->get($shelfBook->getUrl());
$bookVisit->assertElementNotContains('.breadcrumbs', 'Shelves');
$bookVisit->assertElementNotContains('.breadcrumbs', $shelf->getShortName());
$this->get($shelf->getUrl());
$bookVisit = $this->get($shelfBook->getUrl());
$bookVisit->assertElementContains('.breadcrumbs', 'Shelves');
$bookVisit->assertElementContains('.breadcrumbs', $shelf->getShortName());
$pageVisit = $this->get($shelfPage->getUrl());
$pageVisit->assertElementContains('.breadcrumbs', 'Shelves');
$pageVisit->assertElementContains('.breadcrumbs', $shelf->getShortName());
$this->get('/books');
$pageVisit = $this->get($shelfPage->getUrl());
$pageVisit->assertElementNotContains('.breadcrumbs', 'Shelves');
$pageVisit->assertElementNotContains('.breadcrumbs', $shelf->getShortName());
}
}