diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index b65bca51e..bf8165afe 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -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); diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index b1b69814d..975929639 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -569,7 +569,7 @@ class EntityRepo $draftPage->save(); $this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); - + $this->searchService->indexEntity($draftPage); return $draftPage; } diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 7ecfb95c7..ec4889e50 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -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'); } diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index c5baecf16..6a88aa811 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -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) { diff --git a/resources/assets/js/vues/entity-search.js b/resources/assets/js/vues/entity-search.js new file mode 100644 index 000000000..7266bf33d --- /dev/null +++ b/resources/assets/js/vues/entity-search.js @@ -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 +}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index 832a5415d..8cc1dd656 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -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 => { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 6acc47468..051268926 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -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; diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 66c2e8042..8644f7a4a 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -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 diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index f5e08b2f6..adfec4525 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -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> diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index 28c34eef2..d4126cbcc 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -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> diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index f366e9e9b..faae6420a 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -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> diff --git a/routes/web.php b/routes/web.php index dad7a55e5..8ecfd9465 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');