Dan Brown
Committed by GitHub

Merge pull request #340 from BookStackApp/search_system

Implementation of new search system 
Showing 46 changed files with 732 additions and 324 deletions
...@@ -56,4 +56,13 @@ class Book extends Entity ...@@ -56,4 +56,13 @@ class Book extends Entity
56 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; 56 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
57 } 57 }
58 58
59 + /**
60 + * Return a generalised, common raw query that can be 'unioned' across entities.
61 + * @return string
62 + */
63 + public function entityRawQuery()
64 + {
65 + return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
66 + }
67 +
59 } 68 }
......
...@@ -51,4 +51,13 @@ class Chapter extends Entity ...@@ -51,4 +51,13 @@ class Chapter extends Entity
51 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; 51 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
52 } 52 }
53 53
54 + /**
55 + * Return a generalised, common raw query that can be 'unioned' across entities.
56 + * @return string
57 + */
58 + public function entityRawQuery()
59 + {
60 + return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
61 + }
62 +
54 } 63 }
......
...@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command ...@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
12 * 12 *
13 * @var string 13 * @var string
14 */ 14 */
15 - protected $signature = 'bookstack:regenerate-permissions'; 15 + protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
16 16
17 /** 17 /**
18 * The console command description. 18 * The console command description.
...@@ -46,7 +46,14 @@ class RegeneratePermissions extends Command ...@@ -46,7 +46,14 @@ class RegeneratePermissions extends Command
46 */ 46 */
47 public function handle() 47 public function handle()
48 { 48 {
49 + $connection = \DB::getDefaultConnection();
50 + if ($this->option('database') !== null) {
51 + \DB::setDefaultConnection($this->option('database'));
52 + }
53 +
49 $this->permissionService->buildJointPermissions(); 54 $this->permissionService->buildJointPermissions();
55 +
56 + \DB::setDefaultConnection($connection);
50 $this->comment('Permissions regenerated'); 57 $this->comment('Permissions regenerated');
51 } 58 }
52 } 59 }
......
1 +<?php
2 +
3 +namespace BookStack\Console\Commands;
4 +
5 +use BookStack\Services\SearchService;
6 +use Illuminate\Console\Command;
7 +
8 +class RegenerateSearch extends Command
9 +{
10 + /**
11 + * The name and signature of the console command.
12 + *
13 + * @var string
14 + */
15 + protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
16 +
17 + /**
18 + * The console command description.
19 + *
20 + * @var string
21 + */
22 + protected $description = 'Command description';
23 +
24 + protected $searchService;
25 +
26 + /**
27 + * Create a new command instance.
28 + *
29 + * @param SearchService $searchService
30 + */
31 + public function __construct(SearchService $searchService)
32 + {
33 + parent::__construct();
34 + $this->searchService = $searchService;
35 + }
36 +
37 + /**
38 + * Execute the console command.
39 + *
40 + * @return mixed
41 + */
42 + public function handle()
43 + {
44 + $connection = \DB::getDefaultConnection();
45 + if ($this->option('database') !== null) {
46 + \DB::setDefaultConnection($this->option('database'));
47 + }
48 +
49 + $this->searchService->indexAllEntities();
50 + \DB::setDefaultConnection($connection);
51 + $this->comment('Search index regenerated');
52 + }
53 +}
1 -<?php 1 +<?php namespace BookStack\Console;
2 -
3 -namespace BookStack\Console;
4 2
5 use Illuminate\Console\Scheduling\Schedule; 3 use Illuminate\Console\Scheduling\Schedule;
6 use Illuminate\Foundation\Console\Kernel as ConsoleKernel; 4 use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
...@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel ...@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel
13 * @var array 11 * @var array
14 */ 12 */
15 protected $commands = [ 13 protected $commands = [
16 - \BookStack\Console\Commands\ClearViews::class, 14 + Commands\ClearViews::class,
17 - \BookStack\Console\Commands\ClearActivity::class, 15 + Commands\ClearActivity::class,
18 - \BookStack\Console\Commands\ClearRevisions::class, 16 + Commands\ClearRevisions::class,
19 - \BookStack\Console\Commands\RegeneratePermissions::class, 17 + Commands\RegeneratePermissions::class,
18 + Commands\RegenerateSearch::class
20 ]; 19 ];
21 20
22 /** 21 /**
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 class Entity extends Ownable 4 class Entity extends Ownable
5 { 5 {
6 6
7 - protected $fieldsToSearch = ['name', 'description']; 7 + public $textField = 'description';
8 8
9 /** 9 /**
10 * Compares this entity to another given entity. 10 * Compares this entity to another given entity.
...@@ -66,6 +66,15 @@ class Entity extends Ownable ...@@ -66,6 +66,15 @@ class Entity extends Ownable
66 } 66 }
67 67
68 /** 68 /**
69 + * Get the related search terms.
70 + * @return \Illuminate\Database\Eloquent\Relations\MorphMany
71 + */
72 + public function searchTerms()
73 + {
74 + return $this->morphMany(SearchTerm::class, 'entity');
75 + }
76 +
77 + /**
69 * Get this entities restrictions. 78 * Get this entities restrictions.
70 */ 79 */
71 public function permissions() 80 public function permissions()
...@@ -153,67 +162,19 @@ class Entity extends Ownable ...@@ -153,67 +162,19 @@ class Entity extends Ownable
153 } 162 }
154 163
155 /** 164 /**
156 - * Perform a full-text search on this entity. 165 + * Get the body text of this entity.
157 - * @param string[] $fieldsToSearch
158 - * @param string[] $terms
159 - * @param string[] array $wheres
160 * @return mixed 166 * @return mixed
161 */ 167 */
162 - public function fullTextSearchQuery($terms, $wheres = []) 168 + public function getText()
163 { 169 {
164 - $exactTerms = []; 170 + return $this->{$this->textField};
165 - $fuzzyTerms = [];
166 - $search = static::newQuery();
167 -
168 - foreach ($terms as $key => $term) {
169 - $term = htmlentities($term, ENT_QUOTES);
170 - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
171 - if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
172 - $term = str_replace('&quot;', '', $term);
173 - $exactTerms[] = '%' . $term . '%';
174 - } else {
175 - $term = '' . $term . '*';
176 - if ($term !== '*') $fuzzyTerms[] = $term;
177 - }
178 - }
179 -
180 - $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
181 -
182 -
183 - // Perform fulltext search if relevant terms exist.
184 - if ($isFuzzy) {
185 - $termString = implode(' ', $fuzzyTerms);
186 - $fields = implode(',', $this->fieldsToSearch);
187 - $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
188 - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
189 } 171 }
190 172
191 - // Ensure at least one exact term matches if in search 173 + /**
192 - if (count($exactTerms) > 0) { 174 + * Return a generalised, common raw query that can be 'unioned' across entities.
193 - $search = $search->where(function ($query) use ($exactTerms) { 175 + * @return string
194 - foreach ($exactTerms as $exactTerm) { 176 + */
195 - foreach ($this->fieldsToSearch as $field) { 177 + public function entityRawQuery(){return '';}
196 - $query->orWhere($field, 'like', $exactTerm);
197 - }
198 - }
199 - });
200 - }
201 -
202 - $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
203 -
204 - // Add additional where terms
205 - foreach ($wheres as $whereTerm) {
206 - $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
207 - }
208 -
209 - // Load in relations
210 - if ($this->isA('page')) {
211 - $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
212 - } else if ($this->isA('chapter')) {
213 - $search = $search->with('book');
214 - }
215 178
216 - return $search->orderBy($orderBy, 'desc');
217 - }
218 179
219 } 180 }
......
1 <?php namespace BookStack\Http\Controllers; 1 <?php namespace BookStack\Http\Controllers;
2 2
3 use BookStack\Repos\EntityRepo; 3 use BookStack\Repos\EntityRepo;
4 +use BookStack\Services\SearchService;
4 use BookStack\Services\ViewService; 5 use BookStack\Services\ViewService;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 7
...@@ -8,16 +9,19 @@ class SearchController extends Controller ...@@ -8,16 +9,19 @@ class SearchController extends Controller
8 { 9 {
9 protected $entityRepo; 10 protected $entityRepo;
10 protected $viewService; 11 protected $viewService;
12 + protected $searchService;
11 13
12 /** 14 /**
13 * SearchController constructor. 15 * SearchController constructor.
14 * @param EntityRepo $entityRepo 16 * @param EntityRepo $entityRepo
15 * @param ViewService $viewService 17 * @param ViewService $viewService
18 + * @param SearchService $searchService
16 */ 19 */
17 - public function __construct(EntityRepo $entityRepo, ViewService $viewService) 20 + public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
18 { 21 {
19 $this->entityRepo = $entityRepo; 22 $this->entityRepo = $entityRepo;
20 $this->viewService = $viewService; 23 $this->viewService = $viewService;
24 + $this->searchService = $searchService;
21 parent::__construct(); 25 parent::__construct();
22 } 26 }
23 27
...@@ -27,105 +31,55 @@ class SearchController extends Controller ...@@ -27,105 +31,55 @@ class SearchController extends Controller
27 * @return \Illuminate\View\View 31 * @return \Illuminate\View\View
28 * @internal param string $searchTerm 32 * @internal param string $searchTerm
29 */ 33 */
30 - public function searchAll(Request $request) 34 + public function search(Request $request)
31 { 35 {
32 - if (!$request->has('term')) {
33 - return redirect()->back();
34 - }
35 $searchTerm = $request->get('term'); 36 $searchTerm = $request->get('term');
36 - $paginationAppends = $request->only('term');
37 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
38 - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
39 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
40 $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); 37 $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
41 - return view('search/all', [
42 - 'pages' => $pages,
43 - 'books' => $books,
44 - 'chapters' => $chapters,
45 - 'searchTerm' => $searchTerm
46 - ]);
47 - }
48 -
49 - /**
50 - * Search only the pages in the system.
51 - * @param Request $request
52 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
53 - */
54 - public function searchPages(Request $request)
55 - {
56 - if (!$request->has('term')) return redirect()->back();
57 38
58 - $searchTerm = $request->get('term'); 39 + $page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
59 - $paginationAppends = $request->only('term'); 40 + $nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
60 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
61 - $this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
62 - return view('search/entity-search-list', [
63 - 'entities' => $pages,
64 - 'title' => trans('entities.search_results_page'),
65 - 'searchTerm' => $searchTerm
66 - ]);
67 - }
68 41
69 - /** 42 + $results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
70 - * Search only the chapters in the system. 43 + $hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
71 - * @param Request $request
72 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
73 - */
74 - public function searchChapters(Request $request)
75 - {
76 - if (!$request->has('term')) return redirect()->back();
77 44
78 - $searchTerm = $request->get('term'); 45 + return view('search/all', [
79 - $paginationAppends = $request->only('term'); 46 + 'entities' => $results['results'],
80 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends); 47 + 'totalResults' => $results['total'],
81 - $this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm])); 48 + 'searchTerm' => $searchTerm,
82 - return view('search/entity-search-list', [ 49 + 'hasNextPage' => $hasNextPage,
83 - 'entities' => $chapters, 50 + 'nextPageLink' => $nextPageLink
84 - 'title' => trans('entities.search_results_chapter'),
85 - 'searchTerm' => $searchTerm
86 ]); 51 ]);
87 } 52 }
88 53
54 +
89 /** 55 /**
90 - * Search only the books in the system. 56 + * Searches all entities within a book.
91 * @param Request $request 57 * @param Request $request
92 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View 58 + * @param integer $bookId
59 + * @return \Illuminate\View\View
60 + * @internal param string $searchTerm
93 */ 61 */
94 - public function searchBooks(Request $request) 62 + public function searchBook(Request $request, $bookId)
95 { 63 {
96 - if (!$request->has('term')) return redirect()->back(); 64 + $term = $request->get('term', '');
97 - 65 + $results = $this->searchService->searchBook($bookId, $term);
98 - $searchTerm = $request->get('term'); 66 + return view('partials/entity-list', ['entities' => $results]);
99 - $paginationAppends = $request->only('term');
100 - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
101 - $this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
102 - return view('search/entity-search-list', [
103 - 'entities' => $books,
104 - 'title' => trans('entities.search_results_book'),
105 - 'searchTerm' => $searchTerm
106 - ]);
107 } 67 }
108 68
109 /** 69 /**
110 - * Searches all entities within a book. 70 + * Searches all entities within a chapter.
111 * @param Request $request 71 * @param Request $request
112 - * @param integer $bookId 72 + * @param integer $chapterId
113 * @return \Illuminate\View\View 73 * @return \Illuminate\View\View
114 * @internal param string $searchTerm 74 * @internal param string $searchTerm
115 */ 75 */
116 - public function searchBook(Request $request, $bookId) 76 + public function searchChapter(Request $request, $chapterId)
117 { 77 {
118 - if (!$request->has('term')) { 78 + $term = $request->get('term', '');
119 - return redirect()->back(); 79 + $results = $this->searchService->searchChapter($chapterId, $term);
120 - } 80 + return view('partials/entity-list', ['entities' => $results]);
121 - $searchTerm = $request->get('term');
122 - $searchWhereTerms = [['book_id', '=', $bookId]];
123 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
124 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
125 - return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
126 } 81 }
127 82
128 -
129 /** 83 /**
130 * 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.
131 * Returns the most popular entities if no search is provided. 85 * Returns the most popular entities if no search is provided.
...@@ -134,18 +88,13 @@ class SearchController extends Controller ...@@ -134,18 +88,13 @@ class SearchController extends Controller
134 */ 88 */
135 public function searchEntitiesAjax(Request $request) 89 public function searchEntitiesAjax(Request $request)
136 { 90 {
137 - $entities = collect();
138 $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']);
139 $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;
140 93
141 // Search for entities otherwise show most popular 94 // Search for entities otherwise show most popular
142 if ($searchTerm !== false) { 95 if ($searchTerm !== false) {
143 - foreach (['page', 'chapter', 'book'] as $entityType) { 96 + $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
144 - if ($entityTypes->contains($entityType)) { 97 + $entities = $this->searchService->searchEntities($searchTerm)['results'];
145 - $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
146 - }
147 - }
148 - $entities = $entities->sortByDesc('title_relevance');
149 } else { 98 } else {
150 $entityNames = $entityTypes->map(function ($type) { 99 $entityNames = $entityTypes->map(function ($type) {
151 return 'BookStack\\' . ucfirst($type); 100 return 'BookStack\\' . ucfirst($type);
......
...@@ -8,8 +8,7 @@ class Page extends Entity ...@@ -8,8 +8,7 @@ class Page extends Entity
8 protected $simpleAttributes = ['name', 'id', 'slug']; 8 protected $simpleAttributes = ['name', 'id', 'slug'];
9 9
10 protected $with = ['book']; 10 protected $with = ['book'];
11 - 11 + public $textField = 'text';
12 - protected $fieldsToSearch = ['name', 'text'];
13 12
14 /** 13 /**
15 * Converts this page into a simplified array. 14 * Converts this page into a simplified array.
...@@ -96,4 +95,14 @@ class Page extends Entity ...@@ -96,4 +95,14 @@ class Page extends Entity
96 return mb_convert_encoding($text, 'UTF-8'); 95 return mb_convert_encoding($text, 'UTF-8');
97 } 96 }
98 97
98 + /**
99 + * Return a generalised, common raw query that can be 'unioned' across entities.
100 + * @param bool $withContent
101 + * @return string
102 + */
103 + public function entityRawQuery($withContent = false)
104 + { $htmlQuery = $withContent ? 'html' : "'' as html";
105 + return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
106 + }
107 +
99 } 108 }
......
1 +<?php namespace BookStack;
2 +
3 +class SearchTerm extends Model
4 +{
5 +
6 + protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
7 + public $timestamps = false;
8 +
9 + /**
10 + * Get the entity that this term belongs to
11 + * @return \Illuminate\Database\Eloquent\Relations\MorphTo
12 + */
13 + public function entity()
14 + {
15 + return $this->morphTo('entity');
16 + }
17 +
18 +}
...@@ -479,8 +479,7 @@ class PermissionService ...@@ -479,8 +479,7 @@ class PermissionService
479 * @return \Illuminate\Database\Query\Builder 479 * @return \Illuminate\Database\Query\Builder
480 */ 480 */
481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { 481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
482 - $pageContentSelect = $fetchPageContent ? 'html' : "''"; 482 + $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
483 - $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
484 $query->where('draft', '=', 0); 483 $query->where('draft', '=', 0);
485 if (!$filterDrafts) { 484 if (!$filterDrafts) {
486 $query->orWhere(function($query) { 485 $query->orWhere(function($query) {
...@@ -488,7 +487,7 @@ class PermissionService ...@@ -488,7 +487,7 @@ class PermissionService
488 }); 487 });
489 } 488 }
490 }); 489 });
491 - $chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id); 490 + $chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
492 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) 491 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
493 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect); 492 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
494 493
...@@ -514,7 +513,7 @@ class PermissionService ...@@ -514,7 +513,7 @@ class PermissionService
514 * @param string $entityType 513 * @param string $entityType
515 * @param Builder|Entity $query 514 * @param Builder|Entity $query
516 * @param string $action 515 * @param string $action
517 - * @return mixed 516 + * @return Builder
518 */ 517 */
519 public function enforceEntityRestrictions($entityType, $query, $action = 'view') 518 public function enforceEntityRestrictions($entityType, $query, $action = 'view')
520 { 519 {
...@@ -540,7 +539,7 @@ class PermissionService ...@@ -540,7 +539,7 @@ class PermissionService
540 } 539 }
541 540
542 /** 541 /**
543 - * Filter items that have entities set a a polymorphic relation. 542 + * Filter items that have entities set as a polymorphic relation.
544 * @param $query 543 * @param $query
545 * @param string $tableName 544 * @param string $tableName
546 * @param string $entityIdColumn 545 * @param string $entityIdColumn
......
...@@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration ...@@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration
12 */ 12 */
13 public function up() 13 public function up()
14 { 14 {
15 - DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)'); 15 + $prefix = DB::getTablePrefix();
16 - DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)'); 16 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
17 - DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)'); 17 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
18 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
18 } 19 }
19 20
20 /** 21 /**
......
...@@ -12,9 +12,10 @@ class FulltextWeighting extends Migration ...@@ -12,9 +12,10 @@ class FulltextWeighting extends Migration
12 */ 12 */
13 public function up() 13 public function up()
14 { 14 {
15 - DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)'); 15 + $prefix = DB::getTablePrefix();
16 - DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)'); 16 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
17 - DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)'); 17 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
18 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
18 } 19 }
19 20
20 /** 21 /**
......
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class CreateSearchIndexTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::create('search_terms', function (Blueprint $table) {
17 + $table->increments('id');
18 + $table->string('term', 200);
19 + $table->string('entity_type', 100);
20 + $table->integer('entity_id');
21 + $table->integer('score');
22 +
23 + $table->index('term');
24 + $table->index('entity_type');
25 + $table->index(['entity_type', 'entity_id']);
26 + $table->index('score');
27 + });
28 +
29 + // Drop search indexes
30 + Schema::table('pages', function(Blueprint $table) {
31 + $table->dropIndex('search');
32 + $table->dropIndex('name_search');
33 + });
34 + Schema::table('books', function(Blueprint $table) {
35 + $table->dropIndex('search');
36 + $table->dropIndex('name_search');
37 + });
38 + Schema::table('chapters', function(Blueprint $table) {
39 + $table->dropIndex('search');
40 + $table->dropIndex('name_search');
41 + });
42 +
43 + app(\BookStack\Services\SearchService::class)->indexAllEntities();
44 + }
45 +
46 + /**
47 + * Reverse the migrations.
48 + *
49 + * @return void
50 + */
51 + public function down()
52 + {
53 + $prefix = DB::getTablePrefix();
54 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
55 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
56 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
57 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
58 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
59 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
60 +
61 + Schema::dropIfExists('search_terms');
62 + }
63 +}
...@@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder ...@@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder
16 $user->attachRole($role); 16 $user->attachRole($role);
17 17
18 18
19 - $books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id]) 19 + factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
20 ->each(function($book) use ($user) { 20 ->each(function($book) use ($user) {
21 $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) 21 $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
22 ->each(function($chapter) use ($user, $book){ 22 ->each(function($chapter) use ($user, $book){
...@@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder ...@@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder
28 $book->pages()->saveMany($pages); 28 $book->pages()->saveMany($pages);
29 }); 29 });
30 30
31 - $restrictionService = app(\BookStack\Services\PermissionService::class); 31 + app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
32 - $restrictionService->buildJointPermissions(); 32 + app(\BookStack\Services\SearchService::class)->indexAllEntities();
33 } 33 }
34 } 34 }
......
1 -var elixir = require('laravel-elixir'); 1 +const argv = require('yargs').argv;
2 +const gulp = require('gulp'),
3 + plumber = require('gulp-plumber');
4 +const autoprefixer = require('gulp-autoprefixer');
5 +const uglify = require('gulp-uglify');
6 +const minifycss = require('gulp-clean-css');
7 +const sass = require('gulp-sass');
8 +const browserify = require("browserify");
9 +const source = require('vinyl-source-stream');
10 +const buffer = require('vinyl-buffer');
11 +const babelify = require("babelify");
12 +const watchify = require("watchify");
13 +const envify = require("envify");
14 +const gutil = require("gulp-util");
2 15
3 -elixir(mix => { 16 +if (argv.production) process.env.NODE_ENV = 'production';
4 - mix.sass('styles.scss'); 17 +
5 - mix.sass('print-styles.scss'); 18 +gulp.task('styles', () => {
6 - mix.sass('export-styles.scss'); 19 + let chain = gulp.src(['resources/assets/sass/**/*.scss'])
7 - mix.browserify('global.js', './public/js/common.js'); 20 + .pipe(plumber({
21 + errorHandler: function (error) {
22 + console.log(error.message);
23 + this.emit('end');
24 + }}))
25 + .pipe(sass())
26 + .pipe(autoprefixer('last 2 versions'));
27 + if (argv.production) chain = chain.pipe(minifycss());
28 + return chain.pipe(gulp.dest('public/css/'));
8 }); 29 });
30 +
31 +
32 +function scriptTask(watch=false) {
33 +
34 + let props = {
35 + basedir: 'resources/assets/js',
36 + debug: true,
37 + entries: ['global.js']
38 + };
39 +
40 + let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props);
41 + bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']});
42 + function rebundle() {
43 + let stream = bundler.bundle();
44 + stream = stream.pipe(source('common.js'));
45 + if (argv.production) stream = stream.pipe(buffer()).pipe(uglify());
46 + return stream.pipe(gulp.dest('public/js/'));
47 + }
48 + bundler.on('update', function() {
49 + rebundle();
50 + gutil.log('Rebundle...');
51 + });
52 + bundler.on('log', gutil.log);
53 + return rebundle();
54 +}
55 +
56 +gulp.task('scripts', () => {scriptTask(false)});
57 +gulp.task('scripts-watch', () => {scriptTask(true)});
58 +
59 +gulp.task('default', ['styles', 'scripts-watch'], () => {
60 + gulp.watch("resources/assets/sass/**/*.scss", ['styles']);
61 +});
62 +
63 +gulp.task('build', ['styles', 'scripts']);
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "private": true, 2 "private": true,
3 "scripts": { 3 "scripts": {
4 - "build": "gulp --production", 4 + "build": "gulp build",
5 - "dev": "gulp watch", 5 + "production": "gulp build --production",
6 - "watch": "gulp watch" 6 + "dev": "gulp",
7 + "watch": "gulp"
7 }, 8 },
8 "devDependencies": { 9 "devDependencies": {
10 + "babelify": "^7.3.0",
11 + "browserify": "^14.3.0",
12 + "envify": "^4.0.0",
13 + "gulp": "3.9.1",
14 + "gulp-autoprefixer": "3.1.1",
15 + "gulp-clean-css": "^3.0.4",
16 + "gulp-minify-css": "1.2.4",
17 + "gulp-plumber": "1.1.0",
18 + "gulp-sass": "3.1.0",
19 + "gulp-uglify": "2.1.2",
20 + "vinyl-buffer": "^1.0.0",
21 + "vinyl-source-stream": "^1.1.0",
22 + "watchify": "^3.9.0",
23 + "yargs": "^7.1.0"
24 + },
25 + "dependencies": {
9 "angular": "^1.5.5", 26 "angular": "^1.5.5",
10 "angular-animate": "^1.5.5", 27 "angular-animate": "^1.5.5",
11 "angular-resource": "^1.5.5", 28 "angular-resource": "^1.5.5",
12 "angular-sanitize": "^1.5.5", 29 "angular-sanitize": "^1.5.5",
13 - "angular-ui-sortable": "^0.15.0", 30 + "angular-ui-sortable": "^0.17.0",
31 + "axios": "^0.16.1",
32 + "babel-preset-es2015": "^6.24.1",
33 + "clipboard": "^1.5.16",
14 "dropzone": "^4.0.1", 34 "dropzone": "^4.0.1",
15 - "gulp": "^3.9.0", 35 + "gulp-util": "^3.0.8",
16 - "laravel-elixir": "^6.0.0-11",
17 - "laravel-elixir-browserify-official": "^0.1.3",
18 "marked": "^0.3.5", 36 "marked": "^0.3.5",
19 - "moment": "^2.12.0" 37 + "moment": "^2.12.0",
38 + "vue": "^2.2.6"
20 }, 39 },
21 - "dependencies": { 40 + "browser": {
22 - "clipboard": "^1.5.16" 41 + "vue": "vue/dist/vue.common.js"
23 } 42 }
24 } 43 }
......
1 "use strict"; 1 "use strict";
2 2
3 -import moment from 'moment'; 3 +const moment = require('moment');
4 -import 'moment/locale/en-gb'; 4 +require('moment/locale/en-gb');
5 -import editorOptions from "./pages/page-form"; 5 +const editorOptions = require("./pages/page-form");
6 6
7 moment.locale('en-gb'); 7 moment.locale('en-gb');
8 8
9 -export default function (ngApp, events) { 9 +module.exports = function (ngApp, events) {
10 10
11 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', 11 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
12 function ($scope, $attrs, $http, $timeout, imageManagerService) { 12 function ($scope, $attrs, $http, $timeout, imageManagerService) {
...@@ -259,39 +259,6 @@ export default function (ngApp, events) { ...@@ -259,39 +259,6 @@ export default 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 "use strict"; 1 "use strict";
2 -import DropZone from "dropzone"; 2 +const DropZone = require("dropzone");
3 -import markdown from "marked"; 3 +const markdown = require("marked");
4 4
5 -export default function (ngApp, events) { 5 +module.exports = function (ngApp, events) {
6 6
7 /** 7 /**
8 * Common tab controls using simple jQuery functions. 8 * Common tab controls using simple jQuery functions.
......
1 "use strict"; 1 "use strict";
2 2
3 -// AngularJS - Create application and load components
4 -import angular from "angular";
5 -import "angular-resource";
6 -import "angular-animate";
7 -import "angular-sanitize";
8 -import "angular-ui-sortable";
9 -
10 // Url retrieval function 3 // Url retrieval function
11 window.baseUrl = function(path) { 4 window.baseUrl = function(path) {
12 let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content'); 5 let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
...@@ -15,11 +8,33 @@ window.baseUrl = function(path) { ...@@ -15,11 +8,33 @@ window.baseUrl = function(path) {
15 return basePath + '/' + path; 8 return basePath + '/' + path;
16 }; 9 };
17 10
11 +const Vue = require("vue");
12 +const axios = require("axios");
13 +
14 +let axiosInstance = axios.create({
15 + headers: {
16 + 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
17 + 'baseURL': window.baseUrl('')
18 + }
19 +});
20 +
21 +Vue.prototype.$http = axiosInstance;
22 +
23 +require("./vues/vues");
24 +
25 +
26 +// AngularJS - Create application and load components
27 +const angular = require("angular");
28 +require("angular-resource");
29 +require("angular-animate");
30 +require("angular-sanitize");
31 +require("angular-ui-sortable");
32 +
18 let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); 33 let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
19 34
20 // Translation setup 35 // Translation setup
21 // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system 36 // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
22 -import Translations from "./translations" 37 +const Translations = require("./translations");
23 let translator = new Translations(window.translations); 38 let translator = new Translations(window.translations);
24 window.trans = translator.get.bind(translator); 39 window.trans = translator.get.bind(translator);
25 40
...@@ -47,11 +62,12 @@ class EventManager { ...@@ -47,11 +62,12 @@ class EventManager {
47 } 62 }
48 63
49 window.Events = new EventManager(); 64 window.Events = new EventManager();
65 +Vue.prototype.$events = window.Events;
50 66
51 // Load in angular specific items 67 // Load in angular specific items
52 -import Services from './services'; 68 +const Services = require('./services');
53 -import Directives from './directives'; 69 +const Directives = require('./directives');
54 -import Controllers from './controllers'; 70 +const Controllers = require('./controllers');
55 Services(ngApp, window.Events); 71 Services(ngApp, window.Events);
56 Directives(ngApp, window.Events); 72 Directives(ngApp, window.Events);
57 Controllers(ngApp, window.Events); 73 Controllers(ngApp, window.Events);
...@@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1 ...@@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1
154 } 170 }
155 171
156 // Page specific items 172 // Page specific items
157 -import "./pages/page-show"; 173 +require("./pages/page-show");
......
...@@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) { ...@@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) {
60 editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']); 60 editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
61 } 61 }
62 62
63 -export default function() { 63 +module.exports = function() {
64 let settings = { 64 let settings = {
65 selector: '#html-editor', 65 selector: '#html-editor',
66 content_css: [ 66 content_css: [
...@@ -213,4 +213,4 @@ export default function() { ...@@ -213,4 +213,4 @@ export default function() {
213 } 213 }
214 }; 214 };
215 return settings; 215 return settings;
216 -}
...\ No newline at end of file ...\ No newline at end of file
216 +};
...\ No newline at end of file ...\ No newline at end of file
......
1 "use strict"; 1 "use strict";
2 // Configure ZeroClipboard 2 // Configure ZeroClipboard
3 -import Clipboard from "clipboard"; 3 +const Clipboard = require("clipboard");
4 4
5 -export default window.setupPageShow = function (pageId) { 5 +let setupPageShow = window.setupPageShow = function (pageId) {
6 6
7 // Set up pointer 7 // Set up pointer
8 let $pointer = $('#pointer').detach(); 8 let $pointer = $('#pointer').detach();
...@@ -151,3 +151,5 @@ export default window.setupPageShow = function (pageId) { ...@@ -151,3 +151,5 @@ export default window.setupPageShow = function (pageId) {
151 }); 151 });
152 152
153 }; 153 };
154 +
155 +module.exports = setupPageShow;
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -44,4 +44,4 @@ class Translator { ...@@ -44,4 +44,4 @@ class Translator {
44 44
45 } 45 }
46 46
47 -export default Translator 47 +module.exports = Translator;
......
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
1 +const moment = require('moment');
2 +
3 +let data = {
4 + terms: '',
5 + termString : '',
6 + search: {
7 + type: {
8 + page: true,
9 + chapter: true,
10 + book: true
11 + },
12 + exactTerms: [],
13 + tagTerms: [],
14 + option: {},
15 + dates: {
16 + updated_after: false,
17 + updated_before: false,
18 + created_after: false,
19 + created_before: false,
20 + }
21 + }
22 +};
23 +
24 +let computed = {
25 +
26 +};
27 +
28 +let methods = {
29 +
30 + appendTerm(term) {
31 + this.termString += ' ' + term;
32 + this.termString = this.termString.replace(/\s{2,}/g, ' ');
33 + this.termString = this.termString.replace(/^\s+/, '');
34 + this.termString = this.termString.replace(/\s+$/, '');
35 + },
36 +
37 + exactParse(searchString) {
38 + this.search.exactTerms = [];
39 + let exactFilter = /"(.+?)"/g;
40 + let matches;
41 + while ((matches = exactFilter.exec(searchString)) !== null) {
42 + this.search.exactTerms.push(matches[1]);
43 + }
44 + },
45 +
46 + exactChange() {
47 + let exactFilter = /"(.+?)"/g;
48 + this.termString = this.termString.replace(exactFilter, '');
49 + let matchesTerm = this.search.exactTerms.filter(term => {
50 + return term.trim() !== '';
51 + }).map(term => {
52 + return `"${term}"`
53 + }).join(' ');
54 + this.appendTerm(matchesTerm);
55 + },
56 +
57 + addExact() {
58 + this.search.exactTerms.push('');
59 + setTimeout(() => {
60 + let exactInputs = document.querySelectorAll('.exact-input');
61 + exactInputs[exactInputs.length - 1].focus();
62 + }, 100);
63 + },
64 +
65 + removeExact(index) {
66 + this.search.exactTerms.splice(index, 1);
67 + this.exactChange();
68 + },
69 +
70 + tagParse(searchString) {
71 + this.search.tagTerms = [];
72 + let tagFilter = /\[(.+?)\]/g;
73 + let matches;
74 + while ((matches = tagFilter.exec(searchString)) !== null) {
75 + this.search.tagTerms.push(matches[1]);
76 + }
77 + },
78 +
79 + tagChange() {
80 + let tagFilter = /\[(.+?)\]/g;
81 + this.termString = this.termString.replace(tagFilter, '');
82 + let matchesTerm = this.search.tagTerms.filter(term => {
83 + return term.trim() !== '';
84 + }).map(term => {
85 + return `[${term}]`
86 + }).join(' ');
87 + this.appendTerm(matchesTerm);
88 + },
89 +
90 + addTag() {
91 + this.search.tagTerms.push('');
92 + setTimeout(() => {
93 + let tagInputs = document.querySelectorAll('.tag-input');
94 + tagInputs[tagInputs.length - 1].focus();
95 + }, 100);
96 + },
97 +
98 + removeTag(index) {
99 + this.search.tagTerms.splice(index, 1);
100 + this.tagChange();
101 + },
102 +
103 + typeParse(searchString) {
104 + let typeFilter = /{\s?type:\s?(.*?)\s?}/;
105 + let match = searchString.match(typeFilter);
106 + let type = this.search.type;
107 + if (!match) {
108 + type.page = type.book = type.chapter = true;
109 + return;
110 + }
111 + let splitTypes = match[1].replace(/ /g, '').split('|');
112 + type.page = (splitTypes.indexOf('page') !== -1);
113 + type.chapter = (splitTypes.indexOf('chapter') !== -1);
114 + type.book = (splitTypes.indexOf('book') !== -1);
115 + },
116 +
117 + typeChange() {
118 + let typeFilter = /{\s?type:\s?(.*?)\s?}/;
119 + let type = this.search.type;
120 + if (type.page === type.chapter && type.page === type.book) {
121 + this.termString = this.termString.replace(typeFilter, '');
122 + return;
123 + }
124 + let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|');
125 + let typeTerm = '{type:'+selectedTypes+'}';
126 + if (this.termString.match(typeFilter)) {
127 + this.termString = this.termString.replace(typeFilter, typeTerm);
128 + return;
129 + }
130 + this.appendTerm(typeTerm);
131 + },
132 +
133 + optionParse(searchString) {
134 + let optionFilter = /{([a-z_\-:]+?)}/gi;
135 + let matches;
136 + while ((matches = optionFilter.exec(searchString)) !== null) {
137 + this.search.option[matches[1].toLowerCase()] = true;
138 + }
139 + },
140 +
141 + optionChange(optionName) {
142 + let isChecked = this.search.option[optionName];
143 + if (isChecked) {
144 + this.appendTerm(`{${optionName}}`);
145 + } else {
146 + this.termString = this.termString.replace(`{${optionName}}`, '');
147 + }
148 + },
149 +
150 + updateSearch(e) {
151 + e.preventDefault();
152 + window.location = '/search?term=' + encodeURIComponent(this.termString);
153 + },
154 +
155 + enableDate(optionName) {
156 + this.search.dates[optionName.toLowerCase()] = moment().format('YYYY-MM-DD');
157 + this.dateChange(optionName);
158 + },
159 +
160 + dateParse(searchString) {
161 + let dateFilter = /{([a-z_\-]+?):([a-z_\-0-9]+?)}/gi;
162 + let dateTags = Object.keys(this.search.dates);
163 + let matches;
164 + while ((matches = dateFilter.exec(searchString)) !== null) {
165 + if (dateTags.indexOf(matches[1]) === -1) continue;
166 + this.search.dates[matches[1].toLowerCase()] = matches[2];
167 + }
168 + },
169 +
170 + dateChange(optionName) {
171 + let dateFilter = new RegExp('{\\s?'+optionName+'\\s?:([a-z_\\-0-9]+?)}', 'gi');
172 + this.termString = this.termString.replace(dateFilter, '');
173 + if (!this.search.dates[optionName]) return;
174 + this.appendTerm(`{${optionName}:${this.search.dates[optionName]}}`);
175 + },
176 +
177 + dateRemove(optionName) {
178 + this.search.dates[optionName] = false;
179 + this.dateChange(optionName);
180 + }
181 +
182 +};
183 +
184 +function created() {
185 + this.termString = document.querySelector('[name=searchTerm]').value;
186 + this.typeParse(this.termString);
187 + this.exactParse(this.termString);
188 + this.tagParse(this.termString);
189 + this.optionParse(this.termString);
190 + this.dateParse(this.termString);
191 +}
192 +
193 +module.exports = {
194 + data, computed, methods, created
195 +};
...\ No newline at end of file ...\ No newline at end of file
1 +const Vue = require("vue");
2 +
3 +function exists(id) {
4 + return document.getElementById(id) !== null;
5 +}
6 +
7 +let vueMapping = {
8 + 'search-system': require('./search'),
9 + 'entity-dashboard': require('./entity-search'),
10 +};
11 +
12 +Object.keys(vueMapping).forEach(id => {
13 + if (exists(id)) {
14 + let config = vueMapping[id];
15 + config.el = '#' + id;
16 + new Vue(config);
17 + }
18 +});
...\ No newline at end of file ...\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 .anim.fadeIn { 2 .anim.fadeIn {
3 opacity: 0; 3 opacity: 0;
4 animation-name: fadeIn; 4 animation-name: fadeIn;
5 - animation-duration: 160ms; 5 + animation-duration: 180ms;
6 animation-timing-function: ease-in-out; 6 animation-timing-function: ease-in-out;
7 animation-fill-mode: forwards; 7 animation-fill-mode: forwards;
8 } 8 }
......
...@@ -98,19 +98,36 @@ label { ...@@ -98,19 +98,36 @@ label {
98 98
99 label.radio, label.checkbox { 99 label.radio, label.checkbox {
100 font-weight: 400; 100 font-weight: 400;
101 + user-select: none;
101 input[type="radio"], input[type="checkbox"] { 102 input[type="radio"], input[type="checkbox"] {
102 margin-right: $-xs; 103 margin-right: $-xs;
103 } 104 }
104 } 105 }
105 106
107 +label.inline.checkbox {
108 + margin-right: $-m;
109 +}
110 +
106 label + p.small { 111 label + p.small {
107 margin-bottom: 0.8em; 112 margin-bottom: 0.8em;
108 } 113 }
109 114
110 -input[type="text"], input[type="number"], input[type="email"], input[type="search"], input[type="url"], input[type="password"], select, textarea { 115 +table.form-table {
116 + max-width: 100%;
117 + td {
118 + overflow: hidden;
119 + padding: $-xxs/2 0;
120 + }
121 +}
122 +
123 +input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
111 @extend .input-base; 124 @extend .input-base;
112 } 125 }
113 126
127 +input[type=date] {
128 + width: 190px;
129 +}
130 +
114 .toggle-switch { 131 .toggle-switch {
115 display: inline-block; 132 display: inline-block;
116 background-color: #BBB; 133 background-color: #BBB;
......
...@@ -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;
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
7 @import "grid"; 7 @import "grid";
8 @import "blocks"; 8 @import "blocks";
9 @import "buttons"; 9 @import "buttons";
10 -@import "forms";
11 @import "tables"; 10 @import "tables";
11 +@import "forms";
12 @import "animations"; 12 @import "animations";
13 @import "tinymce"; 13 @import "tinymce";
14 @import "highlightjs"; 14 @import "highlightjs";
...@@ -17,7 +17,11 @@ ...@@ -17,7 +17,11 @@
17 @import "lists"; 17 @import "lists";
18 @import "pages"; 18 @import "pages";
19 19
20 -[v-cloak], [v-show] {display: none;} 20 +[v-cloak], [v-show] {
21 + display: none; opacity: 0;
22 + animation-name: none !important;
23 +}
24 +
21 25
22 [ng\:cloak], [ng-cloak], .ng-cloak { 26 [ng\:cloak], [ng-cloak], .ng-cloak {
23 display: none !important; 27 display: none !important;
...@@ -272,8 +276,3 @@ $btt-size: 40px; ...@@ -272,8 +276,3 @@ $btt-size: 40px;
272 276
273 277
274 278
275 -
276 -
277 -
278 -
279 -
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Suchergebnisse', 45 'search_results' => 'Suchergebnisse',
46 - 'search_results_page' => 'Seiten-Suchergebnisse',
47 - 'search_results_chapter' => 'Kapitel-Suchergebnisse',
48 - 'search_results_book' => 'Buch-Suchergebnisse',
49 'search_clear' => 'Suche zur&uuml;cksetzen', 46 'search_clear' => 'Suche zur&uuml;cksetzen',
50 - 'search_view_pages' => 'Zeige alle passenden Seiten',
51 - 'search_view_chapters' => 'Zeige alle passenden Kapitel',
52 - 'search_view_books' => 'Zeige alle passenden B&uuml;cher',
53 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden', 47 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
54 'search_for_term' => 'Suche nach :term', 48 'search_for_term' => 'Suche nach :term',
55 - 'search_page_for_term' => 'Suche nach :term in Seiten',
56 - 'search_chapter_for_term' => 'Suche nach :term in Kapiteln',
57 - 'search_book_for_term' => 'Suche nach :term in B&uuml;chern',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -33,6 +33,7 @@ return [ ...@@ -33,6 +33,7 @@ return [
33 'search_clear' => 'Clear Search', 33 'search_clear' => 'Clear Search',
34 'reset' => 'Reset', 34 'reset' => 'Reset',
35 'remove' => 'Remove', 35 'remove' => 'Remove',
36 + 'add' => 'Add',
36 37
37 38
38 /** 39 /**
......
...@@ -43,18 +43,26 @@ return [ ...@@ -43,18 +43,26 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Search Results', 45 'search_results' => 'Search Results',
46 - 'search_results_page' => 'Page Search Results', 46 + 'search_total_results_found' => ':count result found|:count total results found',
47 - 'search_results_chapter' => 'Chapter Search Results',
48 - 'search_results_book' => 'Book Search Results',
49 'search_clear' => 'Clear Search', 47 'search_clear' => 'Clear Search',
50 - 'search_view_pages' => 'View all matches pages',
51 - 'search_view_chapters' => 'View all matches chapters',
52 - 'search_view_books' => 'View all matches books',
53 'search_no_pages' => 'No pages matched this search', 48 'search_no_pages' => 'No pages matched this search',
54 'search_for_term' => 'Search for :term', 49 'search_for_term' => 'Search for :term',
55 - 'search_page_for_term' => 'Page search for :term', 50 + 'search_more' => 'More Results',
56 - 'search_chapter_for_term' => 'Chapter search for :term', 51 + 'search_filters' => 'Search Filters',
57 - 'search_book_for_term' => 'Books search for :term', 52 + 'search_content_type' => 'Content Type',
53 + 'search_exact_matches' => 'Exact Matches',
54 + 'search_tags' => 'Tag Searches',
55 + 'search_viewed_by_me' => 'Viewed by me',
56 + 'search_not_viewed_by_me' => 'Not viewed by me',
57 + 'search_permissions_set' => 'Permissions set',
58 + 'search_created_by_me' => 'Created by me',
59 + 'search_updated_by_me' => 'Updated by me',
60 + 'search_updated_before' => 'Updated before',
61 + 'search_updated_after' => 'Updated after',
62 + 'search_created_before' => 'Created before',
63 + 'search_created_after' => 'Created after',
64 + 'search_set_date' => 'Set Date',
65 + 'search_update' => 'Update Search',
58 66
59 /** 67 /**
60 * Books 68 * Books
...@@ -112,6 +120,7 @@ return [ ...@@ -112,6 +120,7 @@ return [
112 'chapters_empty' => 'No pages are currently in this chapter.', 120 'chapters_empty' => 'No pages are currently in this chapter.',
113 'chapters_permissions_active' => 'Chapter Permissions Active', 121 'chapters_permissions_active' => 'Chapter Permissions Active',
114 'chapters_permissions_success' => 'Chapter Permissions Updated', 122 'chapters_permissions_success' => 'Chapter Permissions Updated',
123 + 'chapters_search_this' => 'Search this chapter',
115 124
116 /** 125 /**
117 * Pages 126 * Pages
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Buscar resultados', 45 'search_results' => 'Buscar resultados',
46 - 'search_results_page' => 'resultados de búsqueda en página',
47 - 'search_results_chapter' => 'Resultados de búsqueda en capítulo ',
48 - 'search_results_book' => 'Resultados de búsqueda en libro',
49 'search_clear' => 'Limpiar resultados', 46 'search_clear' => 'Limpiar resultados',
50 - 'search_view_pages' => 'Ver todas las páginas que concuerdan',
51 - 'search_view_chapters' => 'Ver todos los capítulos que concuerdan',
52 - 'search_view_books' => 'Ver todos los libros que concuerdan',
53 'search_no_pages' => 'Ninguna página encontrada para la búsqueda', 47 'search_no_pages' => 'Ninguna página encontrada para la búsqueda',
54 'search_for_term' => 'Busqueda por :term', 48 'search_for_term' => 'Busqueda por :term',
55 - 'search_page_for_term' => 'Búsqueda de página por :term',
56 - 'search_chapter_for_term' => 'Búsqueda por capítulo de :term',
57 - 'search_book_for_term' => 'Búsqueda en libro de :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Résultats de recherche', 45 'search_results' => 'Résultats de recherche',
46 - 'search_results_page' => 'Résultats de recherche des pages',
47 - 'search_results_chapter' => 'Résultats de recherche des chapitres',
48 - 'search_results_book' => 'Résultats de recherche des livres',
49 'search_clear' => 'Réinitialiser la recherche', 46 'search_clear' => 'Réinitialiser la recherche',
50 - 'search_view_pages' => 'Voir toutes les pages correspondantes',
51 - 'search_view_chapters' => 'Voir tous les chapitres correspondants',
52 - 'search_view_books' => 'Voir tous les livres correspondants',
53 'search_no_pages' => 'Aucune page correspondant à cette recherche', 47 'search_no_pages' => 'Aucune page correspondant à cette recherche',
54 'search_for_term' => 'recherche pour :term', 48 'search_for_term' => 'recherche pour :term',
55 - 'search_page_for_term' => 'Recherche de page pour :term',
56 - 'search_chapter_for_term' => 'Recherche de chapitre pour :term',
57 - 'search_book_for_term' => 'Recherche de livres pour :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Zoekresultaten', 45 'search_results' => 'Zoekresultaten',
46 - 'search_results_page' => 'Pagina Zoekresultaten',
47 - 'search_results_chapter' => 'Hoofdstuk Zoekresultaten',
48 - 'search_results_book' => 'Boek Zoekresultaten',
49 'search_clear' => 'Zoekopdracht wissen', 46 'search_clear' => 'Zoekopdracht wissen',
50 - 'search_view_pages' => 'Bekijk alle gevonden pagina\'s',
51 - 'search_view_chapters' => 'Bekijk alle gevonden hoofdstukken',
52 - 'search_view_books' => 'Bekijk alle gevonden boeken',
53 'search_no_pages' => 'Er zijn geen pagina\'s gevonden', 47 'search_no_pages' => 'Er zijn geen pagina\'s gevonden',
54 'search_for_term' => 'Zoeken op :term', 48 'search_for_term' => 'Zoeken op :term',
55 - 'search_page_for_term' => 'Pagina doorzoeken op :term',
56 - 'search_chapter_for_term' => 'Hoofdstuk doorzoeken op :term',
57 - 'search_book_for_term' => 'Boeken doorzoeken op :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Resultado(s) da Pesquisa', 45 'search_results' => 'Resultado(s) da Pesquisa',
46 - 'search_results_page' => 'Resultado(s) de Pesquisa de Página',
47 - 'search_results_chapter' => 'Resultado(s) de Pesquisa de Capítulo',
48 - 'search_results_book' => 'Resultado(s) de Pesquisa de Livro',
49 'search_clear' => 'Limpar Pesquisa', 46 'search_clear' => 'Limpar Pesquisa',
50 - 'search_view_pages' => 'Visualizar todas as páginas correspondentes',
51 - 'search_view_chapters' => 'Visualizar todos os capítulos correspondentes',
52 - 'search_view_books' => 'Visualizar todos os livros correspondentes',
53 'search_no_pages' => 'Nenhuma página corresponde à pesquisa', 47 'search_no_pages' => 'Nenhuma página corresponde à pesquisa',
54 'search_for_term' => 'Pesquisar por :term', 48 'search_for_term' => 'Pesquisar por :term',
55 - 'search_page_for_term' => 'Pesquisar Página por :term',
56 - 'search_chapter_for_term' => 'Pesquisar Capítulo por :term',
57 - 'search_book_for_term' => 'Pesquisar Livros por :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
47 </a> 47 </a>
48 </div> 48 </div>
49 <div class="col-lg-4 col-sm-3 text-center"> 49 <div class="col-lg-4 col-sm-3 text-center">
50 - <form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box"> 50 + <form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
51 <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> 51 <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
52 <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button> 52 <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
53 </form> 53 </form>
......
...@@ -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,10 +47,11 @@ ...@@ -47,10 +47,11 @@
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 + <div class="chapter-content" v-if="!searching">
54 <p class="text-muted">{{ $chapter->description }}</p> 55 <p class="text-muted">{{ $chapter->description }}</p>
55 56
56 @if(count($pages) > 0) 57 @if(count($pages) > 0)
...@@ -80,7 +81,16 @@ ...@@ -80,7 +81,16 @@
80 81
81 @include('partials.entity-meta', ['entity' => $chapter]) 82 @include('partials.entity-meta', ['entity' => $chapter])
82 </div> 83 </div>
83 - <div class="col-md-3 col-md-offset-1"> 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>
92 + </div>
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>
......
...@@ -123,11 +123,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -123,11 +123,9 @@ Route::group(['middleware' => 'auth'], function () {
123 Route::get('/link/{id}', 'PageController@redirectFromLink'); 123 Route::get('/link/{id}', 'PageController@redirectFromLink');
124 124
125 // Search 125 // Search
126 - Route::get('/search/all', 'SearchController@searchAll'); 126 + Route::get('/search', 'SearchController@search');
127 - Route::get('/search/pages', 'SearchController@searchPages');
128 - Route::get('/search/books', 'SearchController@searchBooks');
129 - Route::get('/search/chapters', 'SearchController@searchChapters');
130 Route::get('/search/book/{bookId}', 'SearchController@searchBook'); 127 Route::get('/search/book/{bookId}', 'SearchController@searchBook');
128 + Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
131 129
132 // Other Pages 130 // Other Pages
133 Route::get('/', 'HomeController@index'); 131 Route::get('/', 'HomeController@index');
......
...@@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase ...@@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase
76 public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) { 76 public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
77 return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book); 77 return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
78 } 78 }
79 +
80 + /**
81 + * Create and return a new test page
82 + * @param array $input
83 + * @return Chapter
84 + */
85 + public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
86 + $book = Book::first();
87 + $entityRepo = $this->app[EntityRepo::class];
88 + $draftPage = $entityRepo->getDraftPage($book);
89 + return $entityRepo->publishPageDraft($draftPage, $input);
90 + }
79 } 91 }
...\ No newline at end of file ...\ No newline at end of file
......