Migrated book search to vue-based system.
Updated old tag seached.
Made chapter page layout widths same as book page.
Closes #344
*/
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.
*/
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);
$draftPage->save();
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
-
+ $this->searchService->indexEntity($draftPage);
return $draftPage;
}
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
+use Illuminate\Support\Collection;
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
* @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();
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');
}
}]);
-
- 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) {
--- /dev/null
+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
+};
\ No newline at end of file
}
let vueMapping = {
- 'search-system': require('./search')
+ 'search-system': require('./search'),
+ 'entity-dashboard': require('./entity-search'),
};
Object.keys(vueMapping).forEach(id => {
transition-property: right, border;
border-left: 0px solid #FFF;
background-color: #FFF;
+ max-width: 320px;
&.fixed {
background-color: #FFF;
z-index: 5;
'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
</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)
@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>
<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))
@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>
</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">
</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>
@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>
// 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');