Dan Brown

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
...@@ -61,16 +61,24 @@ class SearchController extends Controller ...@@ -61,16 +61,24 @@ class SearchController extends Controller
61 */ 61 */
62 public function searchBook(Request $request, $bookId) 62 public function searchBook(Request $request, $bookId)
63 { 63 {
64 - if (!$request->has('term')) { 64 + $term = $request->get('term', '');
65 - return redirect()->back(); 65 + $results = $this->searchService->searchBook($bookId, $term);
66 - } 66 + return view('partials/entity-list', ['entities' => $results]);
67 - $searchTerm = $request->get('term');
68 - $searchWhereTerms = [['book_id', '=', $bookId]];
69 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
70 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
71 - return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
72 } 67 }
73 68
69 + /**
70 + * Searches all entities within a chapter.
71 + * @param Request $request
72 + * @param integer $chapterId
73 + * @return \Illuminate\View\View
74 + * @internal param string $searchTerm
75 + */
76 + public function searchChapter(Request $request, $chapterId)
77 + {
78 + $term = $request->get('term', '');
79 + $results = $this->searchService->searchChapter($chapterId, $term);
80 + return view('partials/entity-list', ['entities' => $results]);
81 + }
74 82
75 /** 83 /**
76 * Search for a list of entities and return a partial HTML response of matching entities. 84 * Search for a list of entities and return a partial HTML response of matching entities.
...@@ -80,19 +88,13 @@ class SearchController extends Controller ...@@ -80,19 +88,13 @@ class SearchController extends Controller
80 */ 88 */
81 public function searchEntitiesAjax(Request $request) 89 public function searchEntitiesAjax(Request $request)
82 { 90 {
83 - $entities = collect();
84 $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); 91 $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
85 $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false; 92 $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
86 93
87 // Search for entities otherwise show most popular 94 // Search for entities otherwise show most popular
88 if ($searchTerm !== false) { 95 if ($searchTerm !== false) {
89 - foreach (['page', 'chapter', 'book'] as $entityType) { 96 + $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
90 - if ($entityTypes->contains($entityType)) { 97 + $entities = $this->searchService->searchEntities($searchTerm)['results'];
91 - // TODO - Update to new system
92 - $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
93 - }
94 - }
95 - $entities = $entities->sortByDesc('title_relevance');
96 } else { 98 } else {
97 $entityNames = $entityTypes->map(function ($type) { 99 $entityNames = $entityTypes->map(function ($type) {
98 return 'BookStack\\' . ucfirst($type); 100 return 'BookStack\\' . ucfirst($type);
......
...@@ -569,7 +569,7 @@ class EntityRepo ...@@ -569,7 +569,7 @@ class EntityRepo
569 569
570 $draftPage->save(); 570 $draftPage->save();
571 $this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); 571 $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
572 - 572 + $this->searchService->indexEntity($draftPage);
573 return $draftPage; 573 return $draftPage;
574 } 574 }
575 575
......
...@@ -8,6 +8,7 @@ use BookStack\SearchTerm; ...@@ -8,6 +8,7 @@ use BookStack\SearchTerm;
8 use Illuminate\Database\Connection; 8 use Illuminate\Database\Connection;
9 use Illuminate\Database\Query\Builder; 9 use Illuminate\Database\Query\Builder;
10 use Illuminate\Database\Query\JoinClause; 10 use Illuminate\Database\Query\JoinClause;
11 +use Illuminate\Support\Collection;
11 12
12 class SearchService 13 class SearchService
13 { 14 {
...@@ -86,6 +87,35 @@ class SearchService ...@@ -86,6 +87,35 @@ class SearchService
86 ]; 87 ];
87 } 88 }
88 89
90 +
91 + /**
92 + * Search a book for entities
93 + * @param integer $bookId
94 + * @param string $searchString
95 + * @return Collection
96 + */
97 + public function searchBook($bookId, $searchString)
98 + {
99 + $terms = $this->parseSearchString($searchString);
100 + $results = collect();
101 + $pages = $this->buildEntitySearchQuery($terms, 'page')->where('book_id', '=', $bookId)->take(20)->get();
102 + $chapters = $this->buildEntitySearchQuery($terms, 'chapter')->where('book_id', '=', $bookId)->take(20)->get();
103 + return $results->merge($pages)->merge($chapters)->sortByDesc('score')->take(20);
104 + }
105 +
106 + /**
107 + * Search a book for entities
108 + * @param integer $chapterId
109 + * @param string $searchString
110 + * @return Collection
111 + */
112 + public function searchChapter($chapterId, $searchString)
113 + {
114 + $terms = $this->parseSearchString($searchString);
115 + $pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
116 + return $pages->sortByDesc('score');
117 + }
118 +
89 /** 119 /**
90 * Search across a particular entity type. 120 * Search across a particular entity type.
91 * @param array $terms 121 * @param array $terms
...@@ -97,6 +127,21 @@ class SearchService ...@@ -97,6 +127,21 @@ class SearchService
97 */ 127 */
98 public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false) 128 public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
99 { 129 {
130 + $query = $this->buildEntitySearchQuery($terms, $entityType);
131 + if ($getCount) return $query->count();
132 +
133 + $query = $query->skip(($page-1) * $count)->take($count);
134 + return $query->get();
135 + }
136 +
137 + /**
138 + * Create a search query for an entity
139 + * @param array $terms
140 + * @param string $entityType
141 + * @return \Illuminate\Database\Eloquent\Builder
142 + */
143 + protected function buildEntitySearchQuery($terms, $entityType = 'page')
144 + {
100 $entity = $this->getEntity($entityType); 145 $entity = $this->getEntity($entityType);
101 $entitySelect = $entity->newQuery(); 146 $entitySelect = $entity->newQuery();
102 147
...@@ -137,11 +182,7 @@ class SearchService ...@@ -137,11 +182,7 @@ class SearchService
137 if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue); 182 if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
138 } 183 }
139 184
140 - $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); 185 + return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
141 - if ($getCount) return $query->count();
142 -
143 - $query = $query->skip(($page-1) * $count)->take($count);
144 - return $query->get();
145 } 186 }
146 187
147 188
......
...@@ -259,39 +259,6 @@ module.exports = function (ngApp, events) { ...@@ -259,39 +259,6 @@ module.exports = function (ngApp, events) {
259 259
260 }]); 260 }]);
261 261
262 -
263 - ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
264 - $scope.searching = false;
265 - $scope.searchTerm = '';
266 - $scope.searchResults = '';
267 -
268 - $scope.searchBook = function (e) {
269 - e.preventDefault();
270 - let term = $scope.searchTerm;
271 - if (term.length == 0) return;
272 - $scope.searching = true;
273 - $scope.searchResults = '';
274 - let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
275 - searchUrl += '?term=' + encodeURIComponent(term);
276 - $http.get(searchUrl).then((response) => {
277 - $scope.searchResults = $sce.trustAsHtml(response.data);
278 - });
279 - };
280 -
281 - $scope.checkSearchForm = function () {
282 - if ($scope.searchTerm.length < 1) {
283 - $scope.searching = false;
284 - }
285 - };
286 -
287 - $scope.clearSearch = function () {
288 - $scope.searching = false;
289 - $scope.searchTerm = '';
290 - };
291 -
292 - }]);
293 -
294 -
295 ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', 262 ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
296 function ($scope, $http, $attrs, $interval, $timeout, $sce) { 263 function ($scope, $http, $attrs, $interval, $timeout, $sce) {
297 264
......
1 +let data = {
2 + id: null,
3 + type: '',
4 + searching: false,
5 + searchTerm: '',
6 + searchResults: '',
7 +};
8 +
9 +let computed = {
10 +
11 +};
12 +
13 +let methods = {
14 +
15 + searchBook() {
16 + if (this.searchTerm.trim().length === 0) return;
17 + this.searching = true;
18 + this.searchResults = '';
19 + let url = window.baseUrl(`/search/${this.type}/${this.id}`);
20 + url += `?term=${encodeURIComponent(this.searchTerm)}`;
21 + this.$http.get(url).then(resp => {
22 + this.searchResults = resp.data;
23 + });
24 + },
25 +
26 + checkSearchForm() {
27 + this.searching = this.searchTerm > 0;
28 + },
29 +
30 + clearSearch() {
31 + this.searching = false;
32 + this.searchTerm = '';
33 + }
34 +
35 +};
36 +
37 +function mounted() {
38 + this.id = Number(this.$el.getAttribute('entity-id'));
39 + this.type = this.$el.getAttribute('entity-type');
40 +}
41 +
42 +module.exports = {
43 + data, computed, methods, mounted
44 +};
...\ No newline at end of file ...\ No newline at end of file
...@@ -5,7 +5,8 @@ function exists(id) { ...@@ -5,7 +5,8 @@ function exists(id) {
5 } 5 }
6 6
7 let vueMapping = { 7 let vueMapping = {
8 - 'search-system': require('./search') 8 + 'search-system': require('./search'),
9 + 'entity-dashboard': require('./entity-search'),
9 }; 10 };
10 11
11 Object.keys(vueMapping).forEach(id => { 12 Object.keys(vueMapping).forEach(id => {
......
...@@ -109,6 +109,7 @@ ...@@ -109,6 +109,7 @@
109 transition-property: right, border; 109 transition-property: right, border;
110 border-left: 0px solid #FFF; 110 border-left: 0px solid #FFF;
111 background-color: #FFF; 111 background-color: #FFF;
112 + max-width: 320px;
112 &.fixed { 113 &.fixed {
113 background-color: #FFF; 114 background-color: #FFF;
114 z-index: 5; 115 z-index: 5;
......
...@@ -120,6 +120,7 @@ return [ ...@@ -120,6 +120,7 @@ return [
120 'chapters_empty' => 'No pages are currently in this chapter.', 120 'chapters_empty' => 'No pages are currently in this chapter.',
121 'chapters_permissions_active' => 'Chapter Permissions Active', 121 'chapters_permissions_active' => 'Chapter Permissions Active',
122 'chapters_permissions_success' => 'Chapter Permissions Updated', 122 'chapters_permissions_success' => 'Chapter Permissions Updated',
123 + 'chapters_search_this' => 'Search this chapter',
123 124
124 /** 125 /**
125 * Pages 126 * Pages
......
...@@ -50,15 +50,15 @@ ...@@ -50,15 +50,15 @@
50 </div> 50 </div>
51 51
52 52
53 - <div class="container" id="book-dashboard" ng-controller="BookShowController" book-id="{{ $book->id }}"> 53 + <div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
54 <div class="row"> 54 <div class="row">
55 <div class="col-md-7"> 55 <div class="col-md-7">
56 56
57 <h1>{{$book->name}}</h1> 57 <h1>{{$book->name}}</h1>
58 - <div class="book-content" ng-show="!searching"> 58 + <div class="book-content" v-if="!searching">
59 - <p class="text-muted" ng-non-bindable>{{$book->description}}</p> 59 + <p class="text-muted" v-pre>{{$book->description}}</p>
60 60
61 - <div class="page-list" ng-non-bindable> 61 + <div class="page-list" v-pre>
62 <hr> 62 <hr>
63 @if(count($bookChildren) > 0) 63 @if(count($bookChildren) > 0)
64 @foreach($bookChildren as $childElement) 64 @foreach($bookChildren as $childElement)
...@@ -81,12 +81,12 @@ ...@@ -81,12 +81,12 @@
81 @include('partials.entity-meta', ['entity' => $book]) 81 @include('partials.entity-meta', ['entity' => $book])
82 </div> 82 </div>
83 </div> 83 </div>
84 - <div class="search-results" ng-cloak ng-show="searching"> 84 + <div class="search-results" v-cloak v-if="searching">
85 - <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> 85 + <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>
86 - <div ng-if="!searchResults"> 86 + <div v-if="!searchResults">
87 @include('partials/loading-icon') 87 @include('partials/loading-icon')
88 </div> 88 </div>
89 - <div ng-bind-html="searchResults"></div> 89 + <div v-html="searchResults"></div>
90 </div> 90 </div>
91 91
92 92
...@@ -94,6 +94,7 @@ ...@@ -94,6 +94,7 @@
94 94
95 <div class="col-md-4 col-md-offset-1"> 95 <div class="col-md-4 col-md-offset-1">
96 <div class="margin-top large"></div> 96 <div class="margin-top large"></div>
97 +
97 @if($book->restricted) 98 @if($book->restricted)
98 <p class="text-muted"> 99 <p class="text-muted">
99 @if(userCan('restrictions-manage', $book)) 100 @if(userCan('restrictions-manage', $book))
...@@ -103,14 +104,16 @@ ...@@ -103,14 +104,16 @@
103 @endif 104 @endif
104 </p> 105 </p>
105 @endif 106 @endif
107 +
106 <div class="search-box"> 108 <div class="search-box">
107 - <form ng-submit="searchBook($event)"> 109 + <form v-on:submit="searchBook">
108 - <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}"> 110 + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
109 <button type="submit"><i class="zmdi zmdi-search"></i></button> 111 <button type="submit"><i class="zmdi zmdi-search"></i></button>
110 - <button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> 112 + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
111 </form> 113 </form>
112 </div> 114 </div>
113 - <div class="activity anim fadeIn"> 115 +
116 + <div class="activity">
114 <h3>{{ trans('entities.recent_activity') }}</h3> 117 <h3>{{ trans('entities.recent_activity') }}</h3>
115 @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) 118 @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
116 </div> 119 </div>
......
...@@ -47,40 +47,50 @@ ...@@ -47,40 +47,50 @@
47 </div> 47 </div>
48 48
49 49
50 - <div class="container" ng-non-bindable> 50 + <div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter">
51 <div class="row"> 51 <div class="row">
52 - <div class="col-md-8"> 52 + <div class="col-md-7">
53 <h1>{{ $chapter->name }}</h1> 53 <h1>{{ $chapter->name }}</h1>
54 - <p class="text-muted">{{ $chapter->description }}</p> 54 + <div class="chapter-content" v-if="!searching">
55 + <p class="text-muted">{{ $chapter->description }}</p>
55 56
56 - @if(count($pages) > 0) 57 + @if(count($pages) > 0)
57 - <div class="page-list"> 58 + <div class="page-list">
58 - <hr>
59 - @foreach($pages as $page)
60 - @include('pages/list-item', ['page' => $page])
61 <hr> 59 <hr>
62 - @endforeach 60 + @foreach($pages as $page)
63 - </div> 61 + @include('pages/list-item', ['page' => $page])
64 - @else 62 + <hr>
65 - <hr> 63 + @endforeach
66 - <p class="text-muted">{{ trans('entities.chapters_empty') }}</p> 64 + </div>
67 - <p> 65 + @else
68 - @if(userCan('page-create', $chapter)) 66 + <hr>
69 - <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a> 67 + <p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
70 - @endif 68 + <p>
71 - @if(userCan('page-create', $chapter) && userCan('book-update', $book)) 69 + @if(userCan('page-create', $chapter))
72 - &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp; 70 + <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
73 - @endif 71 + @endif
74 - @if(userCan('book-update', $book)) 72 + @if(userCan('page-create', $chapter) && userCan('book-update', $book))
75 - <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a> 73 + &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
76 - @endif 74 + @endif
77 - </p> 75 + @if(userCan('book-update', $book))
78 - <hr> 76 + <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
79 - @endif 77 + @endif
78 + </p>
79 + <hr>
80 + @endif
80 81
81 - @include('partials.entity-meta', ['entity' => $chapter]) 82 + @include('partials.entity-meta', ['entity' => $chapter])
83 + </div>
84 +
85 + <div class="search-results" v-cloak v-if="searching">
86 + <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>
87 + <div v-if="!searchResults">
88 + @include('partials/loading-icon')
89 + </div>
90 + <div v-html="searchResults"></div>
91 + </div>
82 </div> 92 </div>
83 - <div class="col-md-3 col-md-offset-1"> 93 + <div class="col-md-4 col-md-offset-1">
84 <div class="margin-top large"></div> 94 <div class="margin-top large"></div>
85 @if($book->restricted || $chapter->restricted) 95 @if($book->restricted || $chapter->restricted)
86 <div class="text-muted"> 96 <div class="text-muted">
...@@ -105,7 +115,16 @@ ...@@ -105,7 +115,16 @@
105 </div> 115 </div>
106 @endif 116 @endif
107 117
118 + <div class="search-box">
119 + <form v-on:submit="searchBook">
120 + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}">
121 + <button type="submit"><i class="zmdi zmdi-search"></i></button>
122 + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
123 + </form>
124 + </div>
125 +
108 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) 126 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
127 +
109 </div> 128 </div>
110 </div> 129 </div>
111 </div> 130 </div>
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
3 3
4 @if(isset($page) && $page->tags->count() > 0) 4 @if(isset($page) && $page->tags->count() > 0)
5 <div class="tag-display"> 5 <div class="tag-display">
6 - <h6 class="text-muted">Page Tags</h6> 6 + <h6 class="text-muted">{{ trans('entities.page_tags') }}</h6>
7 <table> 7 <table>
8 <tbody> 8 <tbody>
9 @foreach($page->tags as $tag) 9 @foreach($page->tags as $tag)
10 <tr class="tag"> 10 <tr class="tag">
11 - <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td> 11 + <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
12 - @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 12 + @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
13 </tr> 13 </tr>
14 @endforeach 14 @endforeach
15 </tbody> 15 </tbody>
......
...@@ -125,6 +125,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -125,6 +125,7 @@ Route::group(['middleware' => 'auth'], function () {
125 // Search 125 // Search
126 Route::get('/search', 'SearchController@search'); 126 Route::get('/search', 'SearchController@search');
127 Route::get('/search/book/{bookId}', 'SearchController@searchBook'); 127 Route::get('/search/book/{bookId}', 'SearchController@searchBook');
128 + Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
128 129
129 // Other Pages 130 // Other Pages
130 Route::get('/', 'HomeController@index'); 131 Route::get('/', 'HomeController@index');
......