Added chapter search

Migrated book search to vue-based system.
Updated old tag seached.
Made chapter page layout widths same as book page.
Closes #344
This commit is contained in:
Dan Brown 2017-04-15 19:16:07 +01:00
parent 0e0945ef84
commit dcde599709
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
12 changed files with 179 additions and 99 deletions

View File

@ -61,16 +61,24 @@ class SearchController extends Controller
*/ */
public function searchBook(Request $request, $bookId) public function searchBook(Request $request, $bookId)
{ {
if (!$request->has('term')) { $term = $request->get('term', '');
return redirect()->back(); $results = $this->searchService->searchBook($bookId, $term);
} return view('partials/entity-list', ['entities' => $results]);
$searchTerm = $request->get('term');
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
} }
/**
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/** /**
* Search for a list of entities and return a partial HTML response of matching entities. * Search for a list of entities and return a partial HTML response of matching entities.
@ -80,19 +88,13 @@ class SearchController extends Controller
*/ */
public function searchEntitiesAjax(Request $request) public function searchEntitiesAjax(Request $request)
{ {
$entities = collect();
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false; $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
// Search for entities otherwise show most popular // Search for entities otherwise show most popular
if ($searchTerm !== false) { if ($searchTerm !== false) {
foreach (['page', 'chapter', 'book'] as $entityType) { $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
if ($entityTypes->contains($entityType)) { $entities = $this->searchService->searchEntities($searchTerm)['results'];
// TODO - Update to new system
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance');
} else { } else {
$entityNames = $entityTypes->map(function ($type) { $entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type); return 'BookStack\\' . ucfirst($type);

View File

@ -569,7 +569,7 @@ class EntityRepo
$draftPage->save(); $draftPage->save();
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
$this->searchService->indexEntity($draftPage);
return $draftPage; return $draftPage;
} }

View File

@ -8,6 +8,7 @@ use BookStack\SearchTerm;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
class SearchService class SearchService
{ {
@ -86,6 +87,35 @@ class SearchService
]; ];
} }
/**
* Search a book for entities
* @param integer $bookId
* @param string $searchString
* @return Collection
*/
public function searchBook($bookId, $searchString)
{
$terms = $this->parseSearchString($searchString);
$results = collect();
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('book_id', '=', $bookId)->take(20)->get();
$chapters = $this->buildEntitySearchQuery($terms, 'chapter')->where('book_id', '=', $bookId)->take(20)->get();
return $results->merge($pages)->merge($chapters)->sortByDesc('score')->take(20);
}
/**
* Search a book for entities
* @param integer $chapterId
* @param string $searchString
* @return Collection
*/
public function searchChapter($chapterId, $searchString)
{
$terms = $this->parseSearchString($searchString);
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score');
}
/** /**
* Search across a particular entity type. * Search across a particular entity type.
* @param array $terms * @param array $terms
@ -96,6 +126,21 @@ class SearchService
* @return \Illuminate\Database\Eloquent\Collection|int|static[] * @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/ */
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false) public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType);
if ($getCount) return $query->count();
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
}
/**
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page')
{ {
$entity = $this->getEntity($entityType); $entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery(); $entitySelect = $entity->newQuery();
@ -137,11 +182,7 @@ class SearchService
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue); if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
} }
$query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
if ($getCount) return $query->count();
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
} }

View File

@ -259,39 +259,6 @@ module.exports = function (ngApp, events) {
}]); }]);
ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
$scope.searching = false;
$scope.searchTerm = '';
$scope.searchResults = '';
$scope.searchBook = function (e) {
e.preventDefault();
let term = $scope.searchTerm;
if (term.length == 0) return;
$scope.searching = true;
$scope.searchResults = '';
let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
searchUrl += '?term=' + encodeURIComponent(term);
$http.get(searchUrl).then((response) => {
$scope.searchResults = $sce.trustAsHtml(response.data);
});
};
$scope.checkSearchForm = function () {
if ($scope.searchTerm.length < 1) {
$scope.searching = false;
}
};
$scope.clearSearch = function () {
$scope.searching = false;
$scope.searchTerm = '';
};
}]);
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) { function ($scope, $http, $attrs, $interval, $timeout, $sce) {

View File

@ -0,0 +1,44 @@
let data = {
id: null,
type: '',
searching: false,
searchTerm: '',
searchResults: '',
};
let computed = {
};
let methods = {
searchBook() {
if (this.searchTerm.trim().length === 0) return;
this.searching = true;
this.searchResults = '';
let url = window.baseUrl(`/search/${this.type}/${this.id}`);
url += `?term=${encodeURIComponent(this.searchTerm)}`;
this.$http.get(url).then(resp => {
this.searchResults = resp.data;
});
},
checkSearchForm() {
this.searching = this.searchTerm > 0;
},
clearSearch() {
this.searching = false;
this.searchTerm = '';
}
};
function mounted() {
this.id = Number(this.$el.getAttribute('entity-id'));
this.type = this.$el.getAttribute('entity-type');
}
module.exports = {
data, computed, methods, mounted
};

View File

@ -5,7 +5,8 @@ function exists(id) {
} }
let vueMapping = { let vueMapping = {
'search-system': require('./search') 'search-system': require('./search'),
'entity-dashboard': require('./entity-search'),
}; };
Object.keys(vueMapping).forEach(id => { Object.keys(vueMapping).forEach(id => {

View File

@ -109,6 +109,7 @@
transition-property: right, border; transition-property: right, border;
border-left: 0px solid #FFF; border-left: 0px solid #FFF;
background-color: #FFF; background-color: #FFF;
max-width: 320px;
&.fixed { &.fixed {
background-color: #FFF; background-color: #FFF;
z-index: 5; z-index: 5;

View File

@ -120,6 +120,7 @@ return [
'chapters_empty' => 'No pages are currently in this chapter.', 'chapters_empty' => 'No pages are currently in this chapter.',
'chapters_permissions_active' => 'Chapter Permissions Active', 'chapters_permissions_active' => 'Chapter Permissions Active',
'chapters_permissions_success' => 'Chapter Permissions Updated', 'chapters_permissions_success' => 'Chapter Permissions Updated',
'chapters_search_this' => 'Search this chapter',
/** /**
* Pages * Pages

View File

@ -50,15 +50,15 @@
</div> </div>
<div class="container" id="book-dashboard" ng-controller="BookShowController" book-id="{{ $book->id }}"> <div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
<div class="row"> <div class="row">
<div class="col-md-7"> <div class="col-md-7">
<h1>{{$book->name}}</h1> <h1>{{$book->name}}</h1>
<div class="book-content" ng-show="!searching"> <div class="book-content" v-if="!searching">
<p class="text-muted" ng-non-bindable>{{$book->description}}</p> <p class="text-muted" v-pre>{{$book->description}}</p>
<div class="page-list" ng-non-bindable> <div class="page-list" v-pre>
<hr> <hr>
@if(count($bookChildren) > 0) @if(count($bookChildren) > 0)
@foreach($bookChildren as $childElement) @foreach($bookChildren as $childElement)
@ -81,12 +81,12 @@
@include('partials.entity-meta', ['entity' => $book]) @include('partials.entity-meta', ['entity' => $book])
</div> </div>
</div> </div>
<div class="search-results" ng-cloak ng-show="searching"> <div class="search-results" v-cloak v-if="searching">
<h3 class="text-muted">{{ trans('entities.search_results') }} <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3> <h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
<div ng-if="!searchResults"> <div v-if="!searchResults">
@include('partials/loading-icon') @include('partials/loading-icon')
</div> </div>
<div ng-bind-html="searchResults"></div> <div v-html="searchResults"></div>
</div> </div>
@ -94,6 +94,7 @@
<div class="col-md-4 col-md-offset-1"> <div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div> <div class="margin-top large"></div>
@if($book->restricted) @if($book->restricted)
<p class="text-muted"> <p class="text-muted">
@if(userCan('restrictions-manage', $book)) @if(userCan('restrictions-manage', $book))
@ -103,14 +104,16 @@
@endif @endif
</p> </p>
@endif @endif
<div class="search-box"> <div class="search-box">
<form ng-submit="searchBook($event)"> <form v-on:submit="searchBook">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}"> <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button> <button type="submit"><i class="zmdi zmdi-search"></i></button>
<button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
</form> </form>
</div> </div>
<div class="activity anim fadeIn">
<div class="activity">
<h3>{{ trans('entities.recent_activity') }}</h3> <h3>{{ trans('entities.recent_activity') }}</h3>
@include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
</div> </div>

View File

@ -47,10 +47,11 @@
</div> </div>
<div class="container" ng-non-bindable> <div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter">
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-7">
<h1>{{ $chapter->name }}</h1> <h1>{{ $chapter->name }}</h1>
<div class="chapter-content" v-if="!searching">
<p class="text-muted">{{ $chapter->description }}</p> <p class="text-muted">{{ $chapter->description }}</p>
@if(count($pages) > 0) @if(count($pages) > 0)
@ -80,7 +81,16 @@
@include('partials.entity-meta', ['entity' => $chapter]) @include('partials.entity-meta', ['entity' => $chapter])
</div> </div>
<div class="col-md-3 col-md-offset-1">
<div class="search-results" v-cloak v-if="searching">
<h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
<div v-if="!searchResults">
@include('partials/loading-icon')
</div>
<div v-html="searchResults"></div>
</div>
</div>
<div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div> <div class="margin-top large"></div>
@if($book->restricted || $chapter->restricted) @if($book->restricted || $chapter->restricted)
<div class="text-muted"> <div class="text-muted">
@ -105,7 +115,16 @@
</div> </div>
@endif @endif
<div class="search-box">
<form v-on:submit="searchBook">
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button>
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
</form>
</div>
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,13 +3,13 @@
@if(isset($page) && $page->tags->count() > 0) @if(isset($page) && $page->tags->count() > 0)
<div class="tag-display"> <div class="tag-display">
<h6 class="text-muted">Page Tags</h6> <h6 class="text-muted">{{ trans('entities.page_tags') }}</h6>
<table> <table>
<tbody> <tbody>
@foreach($page->tags as $tag) @foreach($page->tags as $tag)
<tr class="tag"> <tr class="tag">
<td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td> <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
@if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif @if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

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