mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-19 08:42:48 +08:00
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:
parent
0e0945ef84
commit
dcde599709
|
@ -61,16 +61,24 @@ class SearchController extends Controller
|
|||
*/
|
||||
public function searchBook(Request $request, $bookId)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$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]);
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchBook($bookId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -80,19 +88,13 @@ class SearchController extends Controller
|
|||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entities = collect();
|
||||
$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;
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
foreach (['page', 'chapter', 'book'] as $entityType) {
|
||||
if ($entityTypes->contains($entityType)) {
|
||||
// TODO - Update to new system
|
||||
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
|
||||
}
|
||||
}
|
||||
$entities = $entities->sortByDesc('title_relevance');
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
|
|
|
@ -569,7 +569,7 @@ class EntityRepo
|
|||
|
||||
$draftPage->save();
|
||||
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
|
||||
|
||||
$this->searchService->indexEntity($draftPage);
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ use BookStack\SearchTerm;
|
|||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
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.
|
||||
* @param array $terms
|
||||
|
@ -96,6 +126,21 @@ class SearchService
|
|||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||
*/
|
||||
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);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
@ -137,11 +182,7 @@ class SearchService
|
|||
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
|
||||
}
|
||||
|
||||
$query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
if ($getCount) return $query->count();
|
||||
|
||||
$query = $query->skip(($page-1) * $count)->take($count);
|
||||
return $query->get();
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
|
||||
|
||||
|
|
44
resources/assets/js/vues/entity-search.js
Normal file
44
resources/assets/js/vues/entity-search.js
Normal 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
|
||||
};
|
|
@ -5,7 +5,8 @@ function exists(id) {
|
|||
}
|
||||
|
||||
let vueMapping = {
|
||||
'search-system': require('./search')
|
||||
'search-system': require('./search'),
|
||||
'entity-dashboard': require('./entity-search'),
|
||||
};
|
||||
|
||||
Object.keys(vueMapping).forEach(id => {
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
transition-property: right, border;
|
||||
border-left: 0px solid #FFF;
|
||||
background-color: #FFF;
|
||||
max-width: 320px;
|
||||
&.fixed {
|
||||
background-color: #FFF;
|
||||
z-index: 5;
|
||||
|
|
|
@ -120,6 +120,7 @@ return [
|
|||
'chapters_empty' => 'No pages are currently in this chapter.',
|
||||
'chapters_permissions_active' => 'Chapter Permissions Active',
|
||||
'chapters_permissions_success' => 'Chapter Permissions Updated',
|
||||
'chapters_search_this' => 'Search this chapter',
|
||||
|
||||
/**
|
||||
* Pages
|
||||
|
|
|
@ -50,15 +50,15 @@
|
|||
</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="col-md-7">
|
||||
|
||||
<h1>{{$book->name}}</h1>
|
||||
<div class="book-content" ng-show="!searching">
|
||||
<p class="text-muted" ng-non-bindable>{{$book->description}}</p>
|
||||
<div class="book-content" v-if="!searching">
|
||||
<p class="text-muted" v-pre>{{$book->description}}</p>
|
||||
|
||||
<div class="page-list" ng-non-bindable>
|
||||
<div class="page-list" v-pre>
|
||||
<hr>
|
||||
@if(count($bookChildren) > 0)
|
||||
@foreach($bookChildren as $childElement)
|
||||
|
@ -81,12 +81,12 @@
|
|||
@include('partials.entity-meta', ['entity' => $book])
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results" ng-cloak ng-show="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>
|
||||
<div ng-if="!searchResults">
|
||||
<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 ng-bind-html="searchResults"></div>
|
||||
<div v-html="searchResults"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -94,6 +94,7 @@
|
|||
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<div class="margin-top large"></div>
|
||||
|
||||
@if($book->restricted)
|
||||
<p class="text-muted">
|
||||
@if(userCan('restrictions-manage', $book))
|
||||
|
@ -103,14 +104,16 @@
|
|||
@endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="search-box">
|
||||
<form ng-submit="searchBook($event)">
|
||||
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
|
||||
<form v-on:submit="searchBook">
|
||||
<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 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>
|
||||
</div>
|
||||
<div class="activity anim fadeIn">
|
||||
|
||||
<div class="activity">
|
||||
<h3>{{ trans('entities.recent_activity') }}</h3>
|
||||
@include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
|
||||
</div>
|
||||
|
|
|
@ -47,40 +47,50 @@
|
|||
</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="col-md-8">
|
||||
<div class="col-md-7">
|
||||
<h1>{{ $chapter->name }}</h1>
|
||||
<p class="text-muted">{{ $chapter->description }}</p>
|
||||
<div class="chapter-content" v-if="!searching">
|
||||
<p class="text-muted">{{ $chapter->description }}</p>
|
||||
|
||||
@if(count($pages) > 0)
|
||||
<div class="page-list">
|
||||
<hr>
|
||||
@foreach($pages as $page)
|
||||
@include('pages/list-item', ['page' => $page])
|
||||
@if(count($pages) > 0)
|
||||
<div class="page-list">
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<hr>
|
||||
<p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
|
||||
<p>
|
||||
@if(userCan('page-create', $chapter))
|
||||
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
|
||||
@endif
|
||||
@if(userCan('page-create', $chapter) && userCan('book-update', $book))
|
||||
<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>
|
||||
@endif
|
||||
@if(userCan('book-update', $book))
|
||||
<a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
<hr>
|
||||
@endif
|
||||
@foreach($pages as $page)
|
||||
@include('pages/list-item', ['page' => $page])
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<hr>
|
||||
<p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
|
||||
<p>
|
||||
@if(userCan('page-create', $chapter))
|
||||
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
|
||||
@endif
|
||||
@if(userCan('page-create', $chapter) && userCan('book-update', $book))
|
||||
<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>
|
||||
@endif
|
||||
@if(userCan('book-update', $book))
|
||||
<a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
<hr>
|
||||
@endif
|
||||
|
||||
@include('partials.entity-meta', ['entity' => $chapter])
|
||||
@include('partials.entity-meta', ['entity' => $chapter])
|
||||
</div>
|
||||
|
||||
<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-3 col-md-offset-1">
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<div class="margin-top large"></div>
|
||||
@if($book->restricted || $chapter->restricted)
|
||||
<div class="text-muted">
|
||||
|
@ -105,7 +115,16 @@
|
|||
</div>
|
||||
@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])
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
|
||||
@if(isset($page) && $page->tags->count() > 0)
|
||||
<div class="tag-display">
|
||||
<h6 class="text-muted">Page Tags</h6>
|
||||
<h6 class="text-muted">{{ trans('entities.page_tags') }}</h6>
|
||||
<table>
|
||||
<tbody>
|
||||
@foreach($page->tags as $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>
|
||||
@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
|
||||
<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?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
|
|
@ -125,6 +125,7 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
// Search
|
||||
Route::get('/search', 'SearchController@search');
|
||||
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
|
||||
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
|
||||
|
||||
// Other Pages
|
||||
Route::get('/', 'HomeController@index');
|
||||
|
|
Loading…
Reference in New Issue
Block a user