Added experimental breadcrumb traversal

This commit is contained in:
Dan Brown 2019-02-24 15:57:35 +00:00
parent e70423c73f
commit 035a0d8efb
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
17 changed files with 296 additions and 11 deletions

View File

@ -69,6 +69,15 @@ class Book extends Entity
return $this->hasMany(Page::class);
}
/**
* Get the direct child pages of this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function directPages()
{
return $this->pages()->where('chapter_id', '=', '0');
}
/**
* Get all chapters within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany

View File

@ -341,6 +341,18 @@ class EntityRepo
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get the direct children of a book.
* @param Book $book
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getBookDirectChildren(Book $book)
{
$pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
$chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.

View File

@ -3,6 +3,7 @@
use BookStack\Actions\ViewService;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\SearchService;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
class SearchController extends Controller
@ -104,4 +105,45 @@ class SearchController extends Controller
return view('search/entity-ajax-list', ['entities' => $entities]);
}
/**
* Search siblings items in the system.
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|mixed
*/
public function searchSiblings(Request $request)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
$entity = $this->entityRepo->getById($type, $id);
if (!$entity) {
return $this->jsonError(trans('errors.entity_not_found'), 404);
}
$entities = [];
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
$entities = $this->entityRepo->getChapterChildren($entity->chapter);
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
$entities = $this->entityRepo->getBookDirectChildren($entity->book);
}
// Book in shelf
// TODO - When shelve tracking added, Update below if criteria
// Book
if ($entity->isA('book')) {
$entities = $this->entityRepo->getAll('book');
}
// Shelve
// TODO - When shelve tracking added
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}
}

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 24 24"
version="1.1"
id="svg6"
sodipodi:docname="books.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1413"
id="namedview8"
showgrid="false"
inkscape:zoom="19.666667"
inkscape:cx="13.076733"
inkscape:cy="8.7801453"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M 19.252119,1.707627 H 8.6631356 c -0.9706568,0 -1.7648305,0.7941737 -1.7648305,1.7648305 V 17.591101 c 0,0.970657 0.7941737,1.764831 1.7648305,1.764831 H 19.252119 c 0.970656,0 1.76483,-0.794174 1.76483,-1.764831 V 3.4724575 c 0,-0.9706568 -0.794174,-1.7648305 -1.76483,-1.7648305 z M 8.6631356,3.4724575 H 13.075212 V 10.531779 L 10.869173,9.2081571 8.6631356,10.531779 Z"
id="path4"
inkscape:connector-curvature="0"
style="stroke-width:0.88241524" />
<g
id="g836"
transform="translate(30.610169,3.2033898)">
<path
id="path822"
d="M 0,0 H 24 V 24 H 0 Z"
inkscape:connector-curvature="0"
style="fill:none" />
<path
id="path824"
d="M -27.644068,3.4067797 V 17.40678 c 0,1.1 0.9,2 2,2 h 14 v -2 h -14 V 3.4067797 Z"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,60 @@
class BreadcrumbListing {
constructor(elem) {
this.elem = elem;
this.searchInput = elem.querySelector('input');
this.loadingElem = elem.querySelector('.loading-container');
this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
this.toggleElem = elem.querySelector('[dropdown-toggle]');
// this.loadingElem.style.display = 'none';
const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
this.entityType = entityDescriptor[0];
this.entityId = Number(entityDescriptor[1]);
this.toggleElem.addEventListener('click', this.onShow.bind(this));
this.searchInput.addEventListener('input', this.onSearch.bind(this));
}
onShow() {
this.loadEntityView();
}
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';
}
}
loadEntityView() {
this.toggleLoading(true);
const params = {
'entity_id': this.entityId,
'entity_type': this.entityType,
};
window.$http.get('/search/entity/siblings', {params}).then(resp => {
this.entityListElem.innerHTML = resp.data;
}).catch(err => {
console.error(err);
}).then(() => {
this.toggleLoading(false);
this.onSearch();
});
}
toggleLoading(show = false) {
this.loadingElem.style.display = show ? 'block' : 'none';
}
}
export default BreadcrumbListing;

View File

@ -6,7 +6,7 @@ class DropDown {
constructor(elem) {
this.container = elem;
this.menu = elem.querySelector('ul');
this.menu = elem.querySelector('ul, [dropdown-menu]');
this.toggle = elem.querySelector('[dropdown-toggle]');
this.setupListeners();
}

View File

@ -21,7 +21,7 @@ import homepageControl from "./homepage-control";
import headerMobileToggle from "./header-mobile-toggle";
import listSortControl from "./list-sort-control";
import triLayout from "./tri-layout";
import breadcrumbListing from "./breadcrumb-listing";
const componentMapping = {
'dropdown': dropdown,
@ -47,6 +47,7 @@ const componentMapping = {
'header-mobile-toggle': headerMobileToggle,
'list-sort-control': listSortControl,
'tri-layout': triLayout,
'breadcrumb-listing': breadcrumbListing,
};
window.components = {};

View File

@ -220,6 +220,50 @@ header .search-box {
}
}
.breadcrumb-listing {
position: relative;
.breadcrumb-listing-toggle {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
&:hover {
border-color: #DDD;
}
}
.svg-icon {
margin-right: 0;
}
}
.breadcrumb-listing-dropdown {
box-shadow: $bs-med;
overflow: hidden;
min-height: 100px;
width: 240px;
display: none;
position: absolute;
z-index: 80;
right: -$-m;
.breadcrumb-listing-search .svg-icon {
position: absolute;
left: $-s;
top: 11px;
fill: #888;
pointer-events: none;
}
.breadcrumb-listing-entity-list {
max-height: 400px;
overflow-y: scroll;
text-align: left;
}
input {
padding-left: $-xl;
border-radius: 0;
border: 0;
border-bottom: 1px solid #DDD;
}
}
.faded {
a, button, span, span > div {
color: #666;

View File

@ -340,10 +340,6 @@ span.sep {
/**
* Icons
*/
i {
padding-right: $-xs;
}
.svg-icon {
width: 1em;
height: 1em;
@ -351,5 +347,6 @@ i {
position: relative;
bottom: -0.105em;
margin-right: $-xs;
pointer-events: none;
}

View File

@ -38,13 +38,13 @@
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
@if($currentUser->can('book-create-all'))
<a href="{{ baseUrl("/create-book") }}" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.books_create') }}</span>
</a>
@endif
@include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
</div>
</div>

View File

@ -8,6 +8,12 @@
@section('body')
<div class="mb-s">
@include('partials.breadcrumbs', ['crumbs' => [
$book,
]])
</div>
<div class="content-wrap card">
<h1 class="break-text" v-pre>{{$book->name}}</h1>
<div class="book-content" v-show="!searching">

View File

@ -30,7 +30,7 @@
@if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
<a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
<a href="{{ baseUrl('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage'))
<a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
@endif

View File

@ -0,0 +1,13 @@
<div class="breadcrumb-listing" dropdown breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
<div class="breadcrumb-listing-toggle" dropdown-toggle>
<div class="separator">@icon('chevron-right')</div>
</div>
<div dropdown-menu class="breadcrumb-listing-dropdown card">
<div class="breadcrumb-listing-search">
@icon('search')
<input autocomplete="off" type="text" name="entity-search">
</div>
@include('partials.loading-icon')
<div class="breadcrumb-listing-entity-list px-m"></div>
</div>
</div>

View File

@ -1,10 +1,22 @@
<div class="breadcrumbs text-center">
<?php $breadcrumbCount = 0; ?>
{{--Show top level item--}}
@if (count($crumbs) > 0 && $crumbs[0] instanceof \BookStack\Entities\Book)
<a href="{{ baseUrl('/books') }}" class="icon-list-item">
<span>@icon('books')</span>
<span>{{ trans('entities.books') }}</span>
</a>
<?php $breadcrumbCount++; ?>
@endif
@foreach($crumbs as $key => $crumb)
<?php $isEntity = ($crumb instanceof \BookStack\Entities\Entity); ?>
@if (is_null($crumb))
<?php continue; ?>
@endif
@if ($breadcrumbCount !== 0)
@if ($breadcrumbCount !== 0 && !$isEntity)
<div class="separator">@icon('chevron-right')</div>
@endif
@ -17,10 +29,15 @@
<span>@icon($crumb['icon'])</span>
<span>{{ $crumb['text'] }}</span>
</a>
@elseif($crumb instanceof \BookStack\Entities\Entity)
@elseif($isEntity && userCan('view', $crumb))
@if($breadcrumbCount > 0)
@include('partials.breadcrumb-listing', ['entity' => $crumb])
@endif
<a href="{{ $crumb->getUrl() }}" class="text-{{$crumb->getType()}} icon-list-item">
<span>@icon($crumb->getType())</span>
<span>{{ $crumb->getShortName() }}</span>
<span>
{{ $crumb->getShortName() }}
</span>
</a>
@endif
<?php $breadcrumbCount++; ?>

View File

@ -0,0 +1,11 @@
<div class="entity-list {{ $style ?? '' }}">
@if(count($entities) > 0)
@foreach($entities as $index => $entity)
@include('partials.entity-list-item-basic', ['entity' => $entity])
@endforeach
@else
<p class="text-muted empty-text">
{{ $emptyText ?? trans('common.no_items') }}
</p>
@endif
</div>

View File

@ -7,6 +7,7 @@
{{--<div class="toolbar px-xl">--}}
{{--@yield('toolbar')--}}
{{--</div>--}}
{{--TODO - Cleanup toolbar usage--}}
<div class="tri-layout-container mt-m" tri-layout @yield('container-attrs') >

View File

@ -154,6 +154,7 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/search', 'SearchController@search');
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
// Other Pages
Route::get('/', 'HomeController@index');