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

View File

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

View File

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

View File

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

View File

@ -25,10 +25,8 @@ class BreadcrumbListing {
onSearch() { onSearch() {
const input = this.searchInput.value.toLowerCase().trim(); const input = this.searchInput.value.toLowerCase().trim();
const listItems = this.entityListElem.querySelectorAll('.entity-list-item'); const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
console.log(listItems);
for (let listItem of listItems) { for (let listItem of listItems) {
const match = !input || listItem.textContent.toLowerCase().includes(input); const match = !input || listItem.textContent.toLowerCase().includes(input);
console.log(match);
listItem.style.display = match ? 'flex' : 'none'; 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"> <div class="entity-shelf-books grid third gap-y-xs entity-list-item-children">
@foreach($shelf->books as $book) @foreach($shelf->books as $book)
<div> <div>
<a href="{{ $book->getUrl() }}" class="entity-chip text-book"> <a href="{{ $book->getUrl('?shelf=' . $shelf->id) }}" class="entity-chip text-book">
@icon('book') @icon('book')
{{ $book->name }} {{ $book->name }}
</a> </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]); $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());
}
} }