Committed by
GitHub
Merge pull request #340 from BookStackApp/search_system
Implementation of new search system
Showing
46 changed files
with
758 additions
and
350 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 | } | ... | ... |
app/Console/Commands/RegenerateSearch.php
0 → 100644
| 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 = []; | 171 | + } |
| 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('/".*?"/', $term) || is_numeric($term)) { | ||
| 172 | - $term = str_replace('"', '', $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 | - } | ||
| 190 | - | ||
| 191 | - // Ensure at least one exact term matches if in search | ||
| 192 | - if (count($exactTerms) > 0) { | ||
| 193 | - $search = $search->where(function ($query) use ($exactTerms) { | ||
| 194 | - foreach ($exactTerms as $exactTerm) { | ||
| 195 | - foreach ($this->fieldsToSearch as $field) { | ||
| 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 | 172 | ||
| 209 | - // Load in relations | 173 | + /** |
| 210 | - if ($this->isA('page')) { | 174 | + * Return a generalised, common raw query that can be 'unioned' across entities. |
| 211 | - $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); | 175 | + * @return string |
| 212 | - } else if ($this->isA('chapter')) { | 176 | + */ |
| 213 | - $search = $search->with('book'); | 177 | + public function entityRawQuery(){return '';} |
| 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 | 38 | ||
| 49 | - /** | 39 | + $page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1; |
| 50 | - * Search only the pages in the system. | 40 | + $nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1)); |
| 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 | 41 | ||
| 58 | - $searchTerm = $request->get('term'); | 42 | + $results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20); |
| 59 | - $paginationAppends = $request->only('term'); | 43 | + $hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0; |
| 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 | - | ||
| 69 | - /** | ||
| 70 | - * Search only the chapters in the system. | ||
| 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 | } | ... | ... |
This diff is collapsed.
Click to expand it.
app/SearchTerm.php
0 → 100644
| 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 | ... | ... |
app/Services/SearchService.php
0 → 100644
This diff is collapsed.
Click to expand it.
| ... | @@ -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 | ... | ... |
resources/assets/js/vues/entity-search.js
0 → 100644
| 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 |
resources/assets/js/vues/search.js
0 → 100644
| 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 |
resources/assets/js/vues/vues.js
0 → 100644
| 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ücksetzen', | 46 | 'search_clear' => 'Suche zurü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ü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üchern', | ||
| 58 | 49 | ||
| 59 | /** | 50 | /** |
| 60 | * Books | 51 | * Books | ... | ... |
| ... | @@ -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,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 | - <em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em> | 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 | + <em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em> |
| 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> | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -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'); | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment