From 070d4aeb6ca9af987c551aede0e3b37688a0808e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Mar 2017 12:48:44 +0000 Subject: [PATCH 01/14] Started implementation of new search system --- app/Book.php | 9 ++ app/Chapter.php | 9 ++ app/Console/Commands/RegenerateSearch.php | 46 ++++++ app/Console/Kernel.php | 13 +- app/Entity.php | 38 ++++- app/Http/Controllers/SearchController.php | 18 ++- app/Page.php | 13 +- app/Repos/EntityRepo.php | 21 ++- app/SearchTerm.php | 20 +++ app/Services/PermissionService.php | 7 +- app/Services/SearchService.php | 150 ++++++++++++++++++ ...03_19_091553_create_search_index_table.php | 43 +++++ resources/views/search/all.blade.php | 36 ++--- 13 files changed, 372 insertions(+), 51 deletions(-) create mode 100644 app/Console/Commands/RegenerateSearch.php create mode 100644 app/SearchTerm.php create mode 100644 app/Services/SearchService.php create mode 100644 database/migrations/2017_03_19_091553_create_search_index_table.php diff --git a/app/Book.php b/app/Book.php index 91f74ca64..06c00945d 100644 --- a/app/Book.php +++ b/app/Book.php @@ -56,4 +56,13 @@ class Book extends Entity return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; } + /** + * Return a generalised, common raw query that can be 'unioned' across entities. + * @return string + */ + public function entityRawQuery() + { + 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"; + } + } diff --git a/app/Chapter.php b/app/Chapter.php index dc23f5ebd..b08cb913a 100644 --- a/app/Chapter.php +++ b/app/Chapter.php @@ -51,4 +51,13 @@ class Chapter extends Entity return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; } + /** + * Return a generalised, common raw query that can be 'unioned' across entities. + * @return string + */ + public function entityRawQuery() + { + 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"; + } + } diff --git a/app/Console/Commands/RegenerateSearch.php b/app/Console/Commands/RegenerateSearch.php new file mode 100644 index 000000000..ccc2a20e5 --- /dev/null +++ b/app/Console/Commands/RegenerateSearch.php @@ -0,0 +1,46 @@ +searchService = $searchService; + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $this->searchService->indexAllEntities(); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 0112e72ca..4fa0b3c80 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -1,6 +1,4 @@ -morphMany(Tag::class, 'entity')->orderBy('order', 'asc'); } + /** + * Get the related search terms. + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function searchTerms() + { + return $this->morphMany(SearchTerm::class, 'entity'); + } + /** * Get this entities restrictions. */ @@ -152,11 +161,26 @@ class Entity extends Ownable return substr($this->name, 0, $length - 3) . '...'; } + /** + * Get the body text of this entity. + * @return mixed + */ + public function getText() + { + return $this->{$this->textField}; + } + + /** + * Return a generalised, common raw query that can be 'unioned' across entities. + * @return string + */ + public function entityRawQuery(){return '';} + /** * Perform a full-text search on this entity. - * @param string[] $fieldsToSearch * @param string[] $terms * @param string[] array $wheres + * TODO - REMOVE * @return mixed */ public function fullTextSearchQuery($terms, $wheres = []) @@ -178,21 +202,21 @@ class Entity extends Ownable } $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0; - + $fieldsToSearch = ['name', $this->textField]; // Perform fulltext search if relevant terms exist. if ($isFuzzy) { $termString = implode(' ', $fuzzyTerms); - $fields = implode(',', $this->fieldsToSearch); + $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); + $search = $search->whereRaw('MATCH(' . implode(',', $fieldsToSearch ). ') AGAINST(? IN BOOLEAN MODE)', [$termString]); } // Ensure at least one exact term matches if in search if (count($exactTerms) > 0) { - $search = $search->where(function ($query) use ($exactTerms) { + $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { foreach ($exactTerms as $exactTerm) { - foreach ($this->fieldsToSearch as $field) { + foreach ($fieldsToSearch as $field) { $query->orWhere($field, 'like', $exactTerm); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 37aaccece..dca5fa0af 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -1,6 +1,7 @@ entityRepo = $entityRepo; $this->viewService = $viewService; + $this->searchService = $searchService; parent::__construct(); } @@ -33,15 +37,13 @@ class SearchController extends Controller return redirect()->back(); } $searchTerm = $request->get('term'); - $paginationAppends = $request->only('term'); - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends); - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends); - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends); +// $paginationAppends = $request->only('term'); TODO - Check pagination $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); + + $entities = $this->searchService->searchEntities($searchTerm); + return view('search/all', [ - 'pages' => $pages, - 'books' => $books, - 'chapters' => $chapters, + 'entities' => $entities, 'searchTerm' => $searchTerm ]); } diff --git a/app/Page.php b/app/Page.php index b24e7778a..d53e02a0b 100644 --- a/app/Page.php +++ b/app/Page.php @@ -8,8 +8,7 @@ class Page extends Entity protected $simpleAttributes = ['name', 'id', 'slug']; protected $with = ['book']; - - protected $fieldsToSearch = ['name', 'text']; + protected $textField = 'text'; /** * Converts this page into a simplified array. @@ -96,4 +95,14 @@ class Page extends Entity return mb_convert_encoding($text, 'UTF-8'); } + /** + * Return a generalised, common raw query that can be 'unioned' across entities. + * @param bool $withContent + * @return string + */ + public function entityRawQuery($withContent = false) + { $htmlQuery = $withContent ? 'html' : "'' as html"; + 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"; + } + } diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 4db69137f..8e3f68859 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -8,6 +8,7 @@ use BookStack\Page; use BookStack\PageRevision; use BookStack\Services\AttachmentService; use BookStack\Services\PermissionService; +use BookStack\Services\SearchService; use BookStack\Services\ViewService; use Carbon\Carbon; use DOMDocument; @@ -58,6 +59,11 @@ class EntityRepo */ protected $tagRepo; + /** + * @var SearchService + */ + protected $searchService; + /** * Acceptable operators to be used in a query * @var array @@ -65,7 +71,7 @@ class EntityRepo protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; /** - * EntityService constructor. + * EntityRepo constructor. * @param Book $book * @param Chapter $chapter * @param Page $page @@ -73,10 +79,12 @@ class EntityRepo * @param ViewService $viewService * @param PermissionService $permissionService * @param TagRepo $tagRepo + * @param SearchService $searchService */ public function __construct( Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision, - ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo + ViewService $viewService, PermissionService $permissionService, + TagRepo $tagRepo, SearchService $searchService ) { $this->book = $book; @@ -91,6 +99,7 @@ class EntityRepo $this->viewService = $viewService; $this->permissionService = $permissionService; $this->tagRepo = $tagRepo; + $this->searchService = $searchService; } /** @@ -608,6 +617,7 @@ class EntityRepo $entity->updated_by = user()->id; $isChapter ? $book->chapters()->save($entity) : $entity->save(); $this->permissionService->buildJointPermissionsForEntity($entity); + $this->searchService->indexEntity($entity); return $entity; } @@ -628,6 +638,7 @@ class EntityRepo $entityModel->updated_by = user()->id; $entityModel->save(); $this->permissionService->buildJointPermissionsForEntity($entityModel); + $this->searchService->indexEntity($entityModel); return $entityModel; } @@ -961,6 +972,8 @@ class EntityRepo $this->savePageRevision($page, $input['summary']); } + $this->searchService->indexEntity($page); + return $page; } @@ -1064,6 +1077,7 @@ class EntityRepo $page->text = strip_tags($page->html); $page->updated_by = user()->id; $page->save(); + $this->searchService->indexEntity($page); return $page; } @@ -1156,6 +1170,7 @@ class EntityRepo $book->views()->delete(); $book->permissions()->delete(); $this->permissionService->deleteJointPermissionsForEntity($book); + $this->searchService->deleteEntityTerms($book); $book->delete(); } @@ -1175,6 +1190,7 @@ class EntityRepo $chapter->views()->delete(); $chapter->permissions()->delete(); $this->permissionService->deleteJointPermissionsForEntity($chapter); + $this->searchService->deleteEntityTerms($chapter); $chapter->delete(); } @@ -1190,6 +1206,7 @@ class EntityRepo $page->revisions()->delete(); $page->permissions()->delete(); $this->permissionService->deleteJointPermissionsForEntity($page); + $this->searchService->deleteEntityTerms($page); // Delete Attached Files $attachmentService = app(AttachmentService::class); diff --git a/app/SearchTerm.php b/app/SearchTerm.php new file mode 100644 index 000000000..a7e3814f8 --- /dev/null +++ b/app/SearchTerm.php @@ -0,0 +1,20 @@ +morphTo('entity'); + } + +} diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 8b47e1246..616c81443 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -479,8 +479,7 @@ class PermissionService * @return \Illuminate\Database\Query\Builder */ public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { - $pageContentSelect = $fetchPageContent ? 'html' : "''"; - $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) { + $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) { $query->where('draft', '=', 0); if (!$filterDrafts) { $query->orWhere(function($query) { @@ -488,7 +487,7 @@ class PermissionService }); } }); - $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); + $chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id); $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) ->mergeBindings($pageSelect)->mergeBindings($chapterSelect); @@ -540,7 +539,7 @@ class PermissionService } /** - * Filter items that have entities set a a polymorphic relation. + * Filter items that have entities set as a polymorphic relation. * @param $query * @param string $tableName * @param string $entityIdColumn diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php new file mode 100644 index 000000000..be1303ca0 --- /dev/null +++ b/app/Services/SearchService.php @@ -0,0 +1,150 @@ +searchTerm = $searchTerm; + $this->book = $book; + $this->chapter = $chapter; + $this->page = $page; + $this->db = $db; + } + + public function searchEntities($searchString, $entityType = 'all') + { + // TODO - Add Tag Searches + // TODO - Add advanced custom column searches + // TODO - Add exact match searches ("") + + $termArray = explode(' ', $searchString); + + $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); + $subQuery->where(function($query) use ($termArray) { + foreach ($termArray as $inputTerm) { + $query->orWhere('term', 'like', $inputTerm .'%'); + } + }); + + $subQuery = $subQuery->groupBy('entity_type', 'entity_id'); + $pageSelect = $this->db->table('pages as e')->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { + $join->on('e.id', '=', 's.entity_id'); + })->selectRaw('e.*, s.score')->orderBy('score', 'desc'); + $pageSelect->mergeBindings($subQuery); + dd($pageSelect->toSql()); + // TODO - Continue from here + } + + /** + * Index the given entity. + * @param Entity $entity + */ + public function indexEntity(Entity $entity) + { + $this->deleteEntityTerms($entity); + $nameTerms = $this->generateTermArrayFromText($entity->name, 5); + $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1); + $terms = array_merge($nameTerms, $bodyTerms); + $entity->searchTerms()->createMany($terms); + } + + /** + * Index multiple Entities at once + * @param Entity[] $entities + */ + protected function indexEntities($entities) { + $terms = []; + foreach ($entities as $entity) { + $nameTerms = $this->generateTermArrayFromText($entity->name, 5); + $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1); + foreach (array_merge($nameTerms, $bodyTerms) as $term) { + $term['entity_id'] = $entity->id; + $term['entity_type'] = $entity->getMorphClass(); + $terms[] = $term; + } + } + $this->searchTerm->insert($terms); + } + + /** + * Delete and re-index the terms for all entities in the system. + */ + public function indexAllEntities() + { + $this->searchTerm->truncate(); + + // Chunk through all books + $this->book->chunk(500, function ($books) { + $this->indexEntities($books); + }); + + // Chunk through all chapters + $this->chapter->chunk(500, function ($chapters) { + $this->indexEntities($chapters); + }); + + // Chunk through all pages + $this->page->chunk(500, function ($pages) { + $this->indexEntities($pages); + }); + } + + /** + * Delete related Entity search terms. + * @param Entity $entity + */ + public function deleteEntityTerms(Entity $entity) + { + $entity->searchTerms()->delete(); + } + + /** + * Create a scored term array from the given text. + * @param $text + * @param float|int $scoreAdjustment + * @return array + */ + protected function generateTermArrayFromText($text, $scoreAdjustment = 1) + { + $tokenMap = []; // {TextToken => OccurrenceCount} + $splitText = explode(' ', $text); + foreach ($splitText as $token) { + if ($token === '') continue; + if (!isset($tokenMap[$token])) $tokenMap[$token] = 0; + $tokenMap[$token]++; + } + + $terms = []; + foreach ($tokenMap as $token => $count) { + $terms[] = [ + 'term' => $token, + 'score' => $count * $scoreAdjustment + ]; + } + return $terms; + } + +} \ No newline at end of file diff --git a/database/migrations/2017_03_19_091553_create_search_index_table.php b/database/migrations/2017_03_19_091553_create_search_index_table.php new file mode 100644 index 000000000..288283958 --- /dev/null +++ b/database/migrations/2017_03_19_091553_create_search_index_table.php @@ -0,0 +1,43 @@ +increments('id'); + $table->string('term', 200); + $table->string('entity_type', 100); + $table->integer('entity_id'); + $table->integer('score'); + + $table->index('term'); + $table->index('entity_type'); + $table->index(['entity_type', 'entity_id']); + $table->index('score'); + }); + + // TODO - Drop old fulltext indexes + + app(\BookStack\Services\SearchService::class)->indexAllEntities(); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('search_terms'); + } +} diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index d4053752f..54626daf1 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -20,36 +20,30 @@

{{ trans('entities.search_results') }}

- @if(count($pages) > 0) - {{ trans('entities.search_view_pages') }} - @endif + {{--TODO - Remove these pages--}} + Remove these links (Commented out) + {{--@if(count($pages) > 0)--}} + {{--{{ trans('entities.search_view_pages') }}--}} + {{--@endif--}} - @if(count($chapters) > 0) -      - {{ trans('entities.search_view_chapters') }} - @endif + {{--@if(count($chapters) > 0)--}} + {{--    --}} + {{--{{ trans('entities.search_view_chapters') }}--}} + {{--@endif--}} - @if(count($books) > 0) -      - {{ trans('entities.search_view_books') }} - @endif + {{--@if(count($books) > 0)--}} + {{--    --}} + {{--{{ trans('entities.search_view_books') }}--}} + {{--@endif--}}

{{ trans('entities.pages') }}

- @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed']) + @include('partials/entity-list', ['entities' => $entities, 'style' => 'detailed'])
- @if(count($books) > 0) -

{{ trans('entities.books') }}

- @include('partials/entity-list', ['entities' => $books]) - @endif - - @if(count($chapters) > 0) -

{{ trans('entities.chapters') }}

- @include('partials/entity-list', ['entities' => $chapters]) - @endif + Sidebar filter controls
From 15524175988bd9d8019d0baa5cbd7965f0b19b91 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 26 Mar 2017 19:24:57 +0100 Subject: [PATCH 02/14] Developed basic search queries. Updated search & permission regen commands with ability to specify database. --- .../Commands/RegeneratePermissions.php | 9 ++- app/Console/Commands/RegenerateSearch.php | 9 ++- app/Services/PermissionService.php | 2 +- app/Services/SearchService.php | 60 +++++++++++++++---- database/seeds/DummyContentSeeder.php | 6 +- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/app/Console/Commands/RegeneratePermissions.php b/app/Console/Commands/RegeneratePermissions.php index 966ee4a82..1dc25f9aa 100644 --- a/app/Console/Commands/RegeneratePermissions.php +++ b/app/Console/Commands/RegeneratePermissions.php @@ -12,7 +12,7 @@ class RegeneratePermissions extends Command * * @var string */ - protected $signature = 'bookstack:regenerate-permissions'; + protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}'; /** * The console command description. @@ -46,7 +46,14 @@ class RegeneratePermissions extends Command */ public function handle() { + $connection = \DB::getDefaultConnection(); + if ($this->option('database') !== null) { + \DB::setDefaultConnection($this->option('database')); + } + $this->permissionService->buildJointPermissions(); + + \DB::setDefaultConnection($connection); $this->comment('Permissions regenerated'); } } diff --git a/app/Console/Commands/RegenerateSearch.php b/app/Console/Commands/RegenerateSearch.php index ccc2a20e5..35ecd46c0 100644 --- a/app/Console/Commands/RegenerateSearch.php +++ b/app/Console/Commands/RegenerateSearch.php @@ -12,7 +12,7 @@ class RegenerateSearch extends Command * * @var string */ - protected $signature = 'bookstack:regenerate-search'; + protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}'; /** * The console command description. @@ -41,6 +41,13 @@ class RegenerateSearch extends Command */ public function handle() { + $connection = \DB::getDefaultConnection(); + if ($this->option('database') !== null) { + \DB::setDefaultConnection($this->option('database')); + } + $this->searchService->indexAllEntities(); + \DB::setDefaultConnection($connection); + $this->comment('Search index regenerated'); } } diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 616c81443..1e75308a0 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -513,7 +513,7 @@ class PermissionService * @param string $entityType * @param Builder|Entity $query * @param string $action - * @return mixed + * @return Builder */ public function enforceEntityRestrictions($entityType, $query, $action = 'view') { diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index be1303ca0..ae8dd008a 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -16,6 +16,8 @@ class SearchService protected $chapter; protected $page; protected $db; + protected $permissionService; + protected $entities; /** * SearchService constructor. @@ -24,22 +26,41 @@ class SearchService * @param Chapter $chapter * @param Page $page * @param Connection $db + * @param PermissionService $permissionService */ - public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db) + public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService) { $this->searchTerm = $searchTerm; $this->book = $book; $this->chapter = $chapter; $this->page = $page; $this->db = $db; + $this->entities = [ + 'page' => $this->page, + 'chapter' => $this->chapter, + 'book' => $this->book + ]; + $this->permissionService = $permissionService; } - public function searchEntities($searchString, $entityType = 'all') + public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) { // TODO - Add Tag Searches // TODO - Add advanced custom column searches // TODO - Add exact match searches ("") + // TODO - Check drafts don't show up in results + // TODO - Move search all page to just /search?term=cat + if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count); + + $bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count); + $chapterSearch = $this->searchEntityTable($searchString, 'chapter', $page, $count); + $pageSearch = $this->searchEntityTable($searchString, 'page', $page, $count); + return collect($bookSearch)->merge($chapterSearch)->merge($pageSearch)->sortByDesc('score'); + } + + public function searchEntityTable($searchString, $entityType = 'page', $page = 0, $count = 20) + { $termArray = explode(' ', $searchString); $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); @@ -49,13 +70,24 @@ class SearchService } }); + $entity = $this->getEntity($entityType); $subQuery = $subQuery->groupBy('entity_type', 'entity_id'); - $pageSelect = $this->db->table('pages as e')->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { - $join->on('e.id', '=', 's.entity_id'); - })->selectRaw('e.*, s.score')->orderBy('score', 'desc'); - $pageSelect->mergeBindings($subQuery); - dd($pageSelect->toSql()); - // TODO - Continue from here + $entitySelect = $entity->newQuery()->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { + $join->on('id', '=', 'entity_id'); + })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc')->skip($page * $count)->take($count); + $entitySelect->mergeBindings($subQuery); + $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); + return $query->get(); + } + + /** + * Get an entity instance via type. + * @param $type + * @return Entity + */ + protected function getEntity($type) + { + return $this->entities[strtolower($type)]; } /** @@ -86,7 +118,11 @@ class SearchService $terms[] = $term; } } - $this->searchTerm->insert($terms); + + $chunkedTerms = array_chunk($terms, 500); + foreach ($chunkedTerms as $termChunk) { + $this->searchTerm->insert($termChunk); + } } /** @@ -97,17 +133,17 @@ class SearchService $this->searchTerm->truncate(); // Chunk through all books - $this->book->chunk(500, function ($books) { + $this->book->chunk(1000, function ($books) { $this->indexEntities($books); }); // Chunk through all chapters - $this->chapter->chunk(500, function ($chapters) { + $this->chapter->chunk(1000, function ($chapters) { $this->indexEntities($chapters); }); // Chunk through all pages - $this->page->chunk(500, function ($pages) { + $this->page->chunk(1000, function ($pages) { $this->indexEntities($pages); }); } diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index efcda4220..6f6b3ddc5 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder $user->attachRole($role); - $books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id]) + factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($book) use ($user) { $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($chapter) use ($user, $book){ @@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder $book->pages()->saveMany($pages); }); - $restrictionService = app(\BookStack\Services\PermissionService::class); - $restrictionService->buildJointPermissions(); + app(\BookStack\Services\PermissionService::class)->buildJointPermissions(); + app(\BookStack\Services\SearchService::class)->indexAllEntities(); } } From 0651eae7ecdeb3b0781ed1231d601d936603e07d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 26 Mar 2017 19:34:53 +0100 Subject: [PATCH 03/14] Improve efficiency of single entity search indexing --- app/Services/SearchService.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index ae8dd008a..ef11de728 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -100,7 +100,11 @@ class SearchService $nameTerms = $this->generateTermArrayFromText($entity->name, 5); $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1); $terms = array_merge($nameTerms, $bodyTerms); - $entity->searchTerms()->createMany($terms); + foreach ($terms as $index => $term) { + $terms[$index]['entity_type'] = $entity->getMorphClass(); + $terms[$index]['entity_id'] = $entity->id; + } + $this->searchTerm->newQuery()->insert($terms); } /** @@ -121,7 +125,7 @@ class SearchService $chunkedTerms = array_chunk($terms, 500); foreach ($chunkedTerms as $termChunk) { - $this->searchTerm->insert($termChunk); + $this->searchTerm->newQuery()->insert($termChunk); } } From 331305333d354dc58dd67d65c6494da83b60ecfe Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Mar 2017 11:57:33 +0100 Subject: [PATCH 04/14] Added search term parsing and exact term matches --- app/Entity.php | 2 +- app/Page.php | 2 +- app/Services/SearchService.php | 99 +++++++++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/app/Entity.php b/app/Entity.php index ae802e31f..52b214eb3 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -4,7 +4,7 @@ class Entity extends Ownable { - protected $textField = 'description'; + public $textField = 'description'; /** * Compares this entity to another given entity. diff --git a/app/Page.php b/app/Page.php index d53e02a0b..c9823e7e4 100644 --- a/app/Page.php +++ b/app/Page.php @@ -8,7 +8,7 @@ class Page extends Entity protected $simpleAttributes = ['name', 'id', 'slug']; protected $with = ['book']; - protected $textField = 'text'; + public $textField = 'text'; /** * Converts this page into a simplified array. diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index ef11de728..2df02bc3e 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -6,7 +6,9 @@ use BookStack\Entity; use BookStack\Page; use BookStack\SearchTerm; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Collection; class SearchService { @@ -43,11 +45,18 @@ class SearchService $this->permissionService = $permissionService; } + /** + * Search all entities in the system. + * @param $searchString + * @param string $entityType + * @param int $page + * @param int $count + * @return Collection + */ public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) { // TODO - Add Tag Searches // TODO - Add advanced custom column searches - // TODO - Add exact match searches ("") // TODO - Check drafts don't show up in results // TODO - Move search all page to just /search?term=cat @@ -59,27 +68,89 @@ class SearchService return collect($bookSearch)->merge($chapterSearch)->merge($pageSearch)->sortByDesc('score'); } + /** + * Search across a particular entity type. + * @param string $searchString + * @param string $entityType + * @param int $page + * @param int $count + * @return \Illuminate\Database\Eloquent\Collection|static[] + */ public function searchEntityTable($searchString, $entityType = 'page', $page = 0, $count = 20) { - $termArray = explode(' ', $searchString); - - $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); - $subQuery->where(function($query) use ($termArray) { - foreach ($termArray as $inputTerm) { - $query->orWhere('term', 'like', $inputTerm .'%'); - } - }); + $searchTerms = $this->parseSearchString($searchString); $entity = $this->getEntity($entityType); - $subQuery = $subQuery->groupBy('entity_type', 'entity_id'); - $entitySelect = $entity->newQuery()->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { - $join->on('id', '=', 'entity_id'); - })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc')->skip($page * $count)->take($count); - $entitySelect->mergeBindings($subQuery); + $entitySelect = $entity->newQuery(); + + // Handle normal search terms + if (count($searchTerms['search']) > 0) { + $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); + $subQuery->where(function(Builder $query) use ($searchTerms) { + foreach ($searchTerms['search'] as $inputTerm) { + $query->orWhere('term', 'like', $inputTerm .'%'); + } + })->groupBy('entity_type', 'entity_id'); + $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { + $join->on('id', '=', 'entity_id'); + })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc'); + $entitySelect->mergeBindings($subQuery); + } + + // Handle exact term matching + if (count($searchTerms['exact']) > 0) { + $entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($searchTerms, $entity) { + foreach ($searchTerms['exact'] as $inputTerm) { + $query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) { + $query->where('name', 'like', '%'.$inputTerm .'%') + ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); + }); + } + }); + } + + $entitySelect->skip($page * $count)->take($count); $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); return $query->get(); } + + /** + * Parse a search string into components. + * @param $searchString + * @return array + */ + public function parseSearchString($searchString) + { + $terms = [ + 'search' => [], + 'exact' => [], + 'tags' => [], + 'filters' => [] + ]; + + $patterns = [ + 'exact' => '/"(.*?)"/', + 'tags' => '/\[(.*?)\]/', + 'filters' => '/\{(.*?)\}/' + ]; + + foreach ($patterns as $termType => $pattern) { + $matches = []; + preg_match_all($pattern, $searchString, $matches); + if (count($matches) > 0) { + $terms[$termType] = $matches[1]; + $searchString = preg_replace($pattern, '', $searchString); + } + } + + foreach (explode(' ', trim($searchString)) as $searchTerm) { + if ($searchTerm !== '') $terms['search'][] = $searchTerm; + } + + return $terms; + } + /** * Get an entity instance via type. * @param $type From 01cb22af37535d9d12d76a56413fdb645568972a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Mar 2017 18:05:34 +0100 Subject: [PATCH 05/14] Added tag searches and advanced filters to new search --- app/Repos/EntityRepo.php | 153 --------------------------------- app/Services/SearchService.php | 148 +++++++++++++++++++++++++++++-- resources/views/base.blade.php | 2 +- routes/web.php | 2 +- 4 files changed, 144 insertions(+), 161 deletions(-) diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 8e3f68859..b1b69814d 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -64,12 +64,6 @@ class EntityRepo */ protected $searchService; - /** - * Acceptable operators to be used in a query - * @var array - */ - protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; - /** * EntityRepo constructor. * @param Book $book @@ -370,56 +364,6 @@ class EntityRepo ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get(); } - /** - * Search entities of a type via a given query. - * @param string $type - * @param string $term - * @param array $whereTerms - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = []) - { - $terms = $this->prepareSearchTerms($term); - $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms)); - $q = $this->addAdvancedSearchQueries($q, $term); - $entities = $q->paginate($count)->appends($paginationAppends); - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - - // Highlight page content - if ($type === 'page') { - //lookahead/behind assertions ensures cut between words - $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words - - foreach ($entities as $page) { - preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER); - //delimiter between occurrences - $results = []; - foreach ($matches as $line) { - $results[] = htmlspecialchars($line[0], 0, 'UTF-8'); - } - $matchLimit = 6; - if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit); - $result = join('... ', $results); - - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $result); - if (strlen($result) < 5) $result = $page->getExcerpt(80); - - $page->searchSnippet = $result; - } - return $entities; - } - - // Highlight chapter/book content - foreach ($entities as $entity) { - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $entity->getExcerpt(100)); - $entity->searchSnippet = $result; - } - return $entities; - } /** * Get the next sequential priority for a new child element in the given book. @@ -501,104 +445,7 @@ class EntityRepo $this->permissionService->buildJointPermissionsForEntity($entity); } - /** - * Prepare a string of search terms by turning - * it into an array of terms. - * Keeps quoted terms together. - * @param $termString - * @return array - */ - public function prepareSearchTerms($termString) - { - $termString = $this->cleanSearchTermString($termString); - preg_match_all('/(".*?")/', $termString, $matches); - $terms = []; - if (count($matches[1]) > 0) { - foreach ($matches[1] as $match) { - $terms[] = $match; - } - $termString = trim(preg_replace('/"(.*?)"/', '', $termString)); - } - if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString)); - return $terms; - } - /** - * Removes any special search notation that should not - * be used in a full-text search. - * @param $termString - * @return mixed - */ - protected function cleanSearchTermString($termString) - { - // Strip tag searches - $termString = preg_replace('/\[.*?\]/', '', $termString); - // Reduced multiple spacing into single spacing - $termString = preg_replace("/\s{2,}/", " ", $termString); - return $termString; - } - - /** - * Get the available query operators as a regex escaped list. - * @return mixed - */ - protected function getRegexEscapedOperators() - { - $escapedOperators = []; - foreach ($this->queryOperators as $operator) { - $escapedOperators[] = preg_quote($operator); - } - return join('|', $escapedOperators); - } - - /** - * Parses advanced search notations and adds them to the db query. - * @param $query - * @param $termString - * @return mixed - */ - protected function addAdvancedSearchQueries($query, $termString) - { - $escapedOperators = $this->getRegexEscapedOperators(); - // Look for tag searches - preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags); - if (count($tags[0]) > 0) { - $this->applyTagSearches($query, $tags); - } - - return $query; - } - - /** - * Apply extracted tag search terms onto a entity query. - * @param $query - * @param $tags - * @return mixed - */ - protected function applyTagSearches($query, $tags) { - $query->where(function($query) use ($tags) { - foreach ($tags[1] as $index => $tagName) { - $query->whereHas('tags', function($query) use ($tags, $index, $tagName) { - $tagOperator = $tags[3][$index]; - $tagValue = $tags[4][$index]; - if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) { - if (is_numeric($tagValue) && $tagOperator !== 'like') { - // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will - // search the value as a string which prevents being able to do number-based operations - // on the tag values. We ensure it has a numeric value and then cast it just to be sure. - $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'"); - $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}"); - } else { - $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue); - } - } else { - $query->where('name', '=', $tagName); - } - }); - } - }); - return $query; - } /** * Create a new entity from request input. diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 2df02bc3e..ec505af00 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -12,7 +12,6 @@ use Illuminate\Support\Collection; class SearchService { - protected $searchTerm; protected $book; protected $chapter; @@ -21,6 +20,12 @@ class SearchService protected $permissionService; protected $entities; + /** + * Acceptable operators to be used in a query + * @var array + */ + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; + /** * SearchService constructor. * @param SearchTerm $searchTerm @@ -55,11 +60,7 @@ class SearchService */ public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) { - // TODO - Add Tag Searches - // TODO - Add advanced custom column searches // TODO - Check drafts don't show up in results - // TODO - Move search all page to just /search?term=cat - if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count); $bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count); @@ -109,6 +110,19 @@ class SearchService }); } + // Handle tag searches + foreach ($searchTerms['tags'] as $inputTerm) { + $this->applyTagSearch($entitySelect, $inputTerm); + } + + // Handle filters + foreach ($searchTerms['filters'] as $filterTerm) { + $splitTerm = explode(':', $filterTerm); + $functionName = camel_case('filter_' . $splitTerm[0]); + $param = count($splitTerm) > 1 ? $splitTerm[1] : ''; + if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $param); + } + $entitySelect->skip($page * $count)->take($count); $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); return $query->get(); @@ -120,7 +134,7 @@ class SearchService * @param $searchString * @return array */ - public function parseSearchString($searchString) + protected function parseSearchString($searchString) { $terms = [ 'search' => [], @@ -151,6 +165,50 @@ class SearchService return $terms; } + /** + * Get the available query operators as a regex escaped list. + * @return mixed + */ + protected function getRegexEscapedOperators() + { + $escapedOperators = []; + foreach ($this->queryOperators as $operator) { + $escapedOperators[] = preg_quote($operator); + } + return join('|', $escapedOperators); + } + + /** + * Apply a tag search term onto a entity query. + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $tagTerm + * @return mixed + */ + protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) { + preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); + $query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) { + $tagName = $tagSplit[1]; + $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; + $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : ''; + $validOperator = in_array($tagOperator, $this->queryOperators); + if (!empty($tagOperator) && !empty($tagValue) && $validOperator) { + if (!empty($tagName)) $query->where('name', '=', $tagName); + if (is_numeric($tagValue) && $tagOperator !== 'like') { + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will + // search the value as a string which prevents being able to do number-based operations + // on the tag values. We ensure it has a numeric value and then cast it just to be sure. + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'"); + $query->whereRaw("value ${tagOperator} ${tagValue}"); + } else { + $query->where('value', $tagOperator, $tagValue); + } + } else { + $query->where('name', '=', $tagName); + } + }); + return $query; + } + /** * Get an entity instance via type. * @param $type @@ -258,4 +316,82 @@ class SearchService return $terms; } + + + + /** + * Custom entity search filters + */ + + protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('updated_at', '>=', $date); + } + + protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('updated_at', '<', $date); + } + + protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('created_at', '>=', $date); + } + + protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('created_at', '<', $date); + } + + protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + if (!is_numeric($input)) return; + $query->where('created_by', '=', $input); + } + + protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + if (!is_numeric($input)) return; + $query->where('updated_by', '=', $input); + } + + protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->where('name', 'like', '%' .$input. '%'); + } + + protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);} + + protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->where($model->textField, 'like', '%' .$input. '%'); + } + + protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->where('restricted', '=', true); + } + + protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->whereHas('views', function($query) { + $query->where('user_id', '=', user()->id); + }); + } + + protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->whereDoesntHave('views', function($query) { + $query->where('user_id', '=', user()->id); + }); + } + } \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 4287014c2..95a9d72b0 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -47,7 +47,7 @@
- diff --git a/routes/web.php b/routes/web.php index 8259a633b..f5ee3f827 100644 --- a/routes/web.php +++ b/routes/web.php @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/link/{id}', 'PageController@redirectFromLink'); // Search - Route::get('/search/all', 'SearchController@searchAll'); + Route::get('/search', 'SearchController@searchAll'); Route::get('/search/pages', 'SearchController@searchPages'); Route::get('/search/books', 'SearchController@searchBooks'); Route::get('/search/chapters', 'SearchController@searchChapters'); From 37813a223a47b63570e975604119a209deac2733 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 9 Apr 2017 14:44:56 +0100 Subject: [PATCH 06/14] Improved DB prefix support and removed old search method --- app/Entity.php | 63 ------------------- .../2015_08_31_175240_add_search_indexes.php | 7 ++- .../2015_12_05_145049_fulltext_weighting.php | 7 ++- ...03_19_091553_create_search_index_table.php | 22 ++++++- 4 files changed, 29 insertions(+), 70 deletions(-) diff --git a/app/Entity.php b/app/Entity.php index 52b214eb3..6aeb66481 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -176,68 +176,5 @@ class Entity extends Ownable */ public function entityRawQuery(){return '';} - /** - * Perform a full-text search on this entity. - * @param string[] $terms - * @param string[] array $wheres - * TODO - REMOVE - * @return mixed - */ - public function fullTextSearchQuery($terms, $wheres = []) - { - $exactTerms = []; - $fuzzyTerms = []; - $search = static::newQuery(); - - foreach ($terms as $key => $term) { - $term = htmlentities($term, ENT_QUOTES); - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); - if (preg_match('/".*?"/', $term) || is_numeric($term)) { - $term = str_replace('"', '', $term); - $exactTerms[] = '%' . $term . '%'; - } else { - $term = '' . $term . '*'; - if ($term !== '*') $fuzzyTerms[] = $term; - } - } - - $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0; - $fieldsToSearch = ['name', $this->textField]; - - // Perform fulltext search if relevant terms exist. - if ($isFuzzy) { - $termString = implode(' ', $fuzzyTerms); - - $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); - $search = $search->whereRaw('MATCH(' . implode(',', $fieldsToSearch ). ') AGAINST(? IN BOOLEAN MODE)', [$termString]); - } - - // Ensure at least one exact term matches if in search - if (count($exactTerms) > 0) { - $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { - foreach ($exactTerms as $exactTerm) { - foreach ($fieldsToSearch as $field) { - $query->orWhere($field, 'like', $exactTerm); - } - } - }); - } - - $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at'; - - // Add additional where terms - foreach ($wheres as $whereTerm) { - $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); - } - - // Load in relations - if ($this->isA('page')) { - $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); - } else if ($this->isA('chapter')) { - $search = $search->with('book'); - } - - return $search->orderBy($orderBy, 'desc'); - } } diff --git a/database/migrations/2015_08_31_175240_add_search_indexes.php b/database/migrations/2015_08_31_175240_add_search_indexes.php index 99e5a28f0..127f69d28 100644 --- a/database/migrations/2015_08_31_175240_add_search_indexes.php +++ b/database/migrations/2015_08_31_175240_add_search_indexes.php @@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration */ public function up() { - DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)'); - DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)'); - DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)'); + $prefix = DB::getTablePrefix(); + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)"); + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)"); + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)"); } /** diff --git a/database/migrations/2015_12_05_145049_fulltext_weighting.php b/database/migrations/2015_12_05_145049_fulltext_weighting.php index cef43f604..998131387 100644 --- a/database/migrations/2015_12_05_145049_fulltext_weighting.php +++ b/database/migrations/2015_12_05_145049_fulltext_weighting.php @@ -12,9 +12,10 @@ class FulltextWeighting extends Migration */ public function up() { - DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)'); - DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)'); - DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)'); + $prefix = DB::getTablePrefix(); + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)"); + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)"); + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)"); } /** diff --git a/database/migrations/2017_03_19_091553_create_search_index_table.php b/database/migrations/2017_03_19_091553_create_search_index_table.php index 288283958..32c6a09e1 100644 --- a/database/migrations/2017_03_19_091553_create_search_index_table.php +++ b/database/migrations/2017_03_19_091553_create_search_index_table.php @@ -26,7 +26,19 @@ class CreateSearchIndexTable extends Migration $table->index('score'); }); - // TODO - Drop old fulltext indexes + // Drop search indexes + Schema::table('pages', function(Blueprint $table) { + $table->dropIndex('search'); + $table->dropIndex('name_search'); + }); + Schema::table('books', function(Blueprint $table) { + $table->dropIndex('search'); + $table->dropIndex('name_search'); + }); + Schema::table('chapters', function(Blueprint $table) { + $table->dropIndex('search'); + $table->dropIndex('name_search'); + }); app(\BookStack\Services\SearchService::class)->indexAllEntities(); } @@ -38,6 +50,14 @@ class CreateSearchIndexTable extends Migration */ public function down() { + $prefix = DB::getTablePrefix(); + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)"); + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)"); + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)"); + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)"); + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)"); + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)"); + Schema::dropIfExists('search_terms'); } } From 1338ae2fc339cfcc0605d1a74db570e08311736c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 9 Apr 2017 20:59:57 +0100 Subject: [PATCH 07/14] Started search interface, Added in vue and moved fonts --- .gitignore | 1 + app/Http/Controllers/SearchController.php | 65 +---------------- app/Services/SearchService.php | 64 +++++++++++------ gulpfile.js | 8 --- package.json | 18 +++-- public/mix-manifest.json | 7 ++ .../fonts/roboto-mono-v4-latin-regular.woff | Bin .../fonts/roboto-mono-v4-latin-regular.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-100.woff | Bin .../fonts/roboto-v15-cyrillic_latin-100.woff2 | Bin .../roboto-v15-cyrillic_latin-100italic.woff | Bin .../roboto-v15-cyrillic_latin-100italic.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-300.woff | Bin .../fonts/roboto-v15-cyrillic_latin-300.woff2 | Bin .../roboto-v15-cyrillic_latin-300italic.woff | Bin .../roboto-v15-cyrillic_latin-300italic.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-500.woff | Bin .../fonts/roboto-v15-cyrillic_latin-500.woff2 | Bin .../roboto-v15-cyrillic_latin-500italic.woff | Bin .../roboto-v15-cyrillic_latin-500italic.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-700.woff | Bin .../fonts/roboto-v15-cyrillic_latin-700.woff2 | Bin .../roboto-v15-cyrillic_latin-700italic.woff | Bin .../roboto-v15-cyrillic_latin-700italic.woff2 | Bin .../roboto-v15-cyrillic_latin-italic.woff | Bin .../roboto-v15-cyrillic_latin-italic.woff2 | Bin .../roboto-v15-cyrillic_latin-regular.woff | Bin .../roboto-v15-cyrillic_latin-regular.woff2 | Bin resources/assets/js/global.js | 30 ++++++-- resources/assets/js/vues/search.js | 66 ++++++++++++++++++ resources/assets/js/vues/vues.js | 16 +++++ resources/assets/sass/_fonts.scss | 44 ++++++------ resources/views/base.blade.php | 1 + resources/views/search/all.blade.php | 46 ++++++------ routes/web.php | 5 +- webpack.mix.js | 18 +++++ 36 files changed, 235 insertions(+), 154 deletions(-) delete mode 100644 gulpfile.js create mode 100644 public/mix-manifest.json rename {public => resources/assets}/fonts/roboto-mono-v4-latin-regular.woff (100%) rename {public => resources/assets}/fonts/roboto-mono-v4-latin-regular.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-100.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-100.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-100italic.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-100italic.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-300.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-300.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-300italic.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-300italic.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-500.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-500.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-500italic.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-500italic.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-700.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-700.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-700italic.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-700italic.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-italic.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-italic.woff2 (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-regular.woff (100%) rename {public => resources/assets}/fonts/roboto-v15-cyrillic_latin-regular.woff2 (100%) create mode 100644 resources/assets/js/vues/search.js create mode 100644 resources/assets/js/vues/vues.js create mode 100644 webpack.mix.js diff --git a/.gitignore b/.gitignore index 5f41a864e..856cf3722 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ Homestead.yaml /public/plugins /public/css /public/js +/public/fonts /public/bower /storage/images _ide_helper.php diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index dca5fa0af..6d29ab17b 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -31,11 +31,8 @@ class SearchController extends Controller * @return \Illuminate\View\View * @internal param string $searchTerm */ - public function searchAll(Request $request) + public function search(Request $request) { - if (!$request->has('term')) { - return redirect()->back(); - } $searchTerm = $request->get('term'); // $paginationAppends = $request->only('term'); TODO - Check pagination $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); @@ -48,65 +45,6 @@ class SearchController extends Controller ]); } - /** - * Search only the pages in the system. - * @param Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View - */ - public function searchPages(Request $request) - { - if (!$request->has('term')) return redirect()->back(); - - $searchTerm = $request->get('term'); - $paginationAppends = $request->only('term'); - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends); - $this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm])); - return view('search/entity-search-list', [ - 'entities' => $pages, - 'title' => trans('entities.search_results_page'), - 'searchTerm' => $searchTerm - ]); - } - - /** - * Search only the chapters in the system. - * @param Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View - */ - public function searchChapters(Request $request) - { - if (!$request->has('term')) return redirect()->back(); - - $searchTerm = $request->get('term'); - $paginationAppends = $request->only('term'); - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends); - $this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm])); - return view('search/entity-search-list', [ - 'entities' => $chapters, - 'title' => trans('entities.search_results_chapter'), - 'searchTerm' => $searchTerm - ]); - } - - /** - * Search only the books in the system. - * @param Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View - */ - public function searchBooks(Request $request) - { - if (!$request->has('term')) return redirect()->back(); - - $searchTerm = $request->get('term'); - $paginationAppends = $request->only('term'); - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends); - $this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm])); - return view('search/entity-search-list', [ - 'entities' => $books, - 'title' => trans('entities.search_results_book'), - 'searchTerm' => $searchTerm - ]); - } /** * Searches all entities within a book. @@ -144,6 +82,7 @@ class SearchController extends Controller if ($searchTerm !== false) { foreach (['page', 'chapter', 'book'] as $entityType) { if ($entityTypes->contains($entityType)) { + // TODO - Update to new system $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items()); } } diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index ec505af00..8202b4997 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -52,7 +52,7 @@ class SearchService /** * Search all entities in the system. - * @param $searchString + * @param string $searchString * @param string $entityType * @param int $page * @param int $count @@ -60,35 +60,45 @@ class SearchService */ public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) { - // TODO - Check drafts don't show up in results - if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count); + $terms = $this->parseSearchString($searchString); + $entityTypes = array_keys($this->entities); + $entityTypesToSearch = $entityTypes; + $results = collect(); - $bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count); - $chapterSearch = $this->searchEntityTable($searchString, 'chapter', $page, $count); - $pageSearch = $this->searchEntityTable($searchString, 'page', $page, $count); - return collect($bookSearch)->merge($chapterSearch)->merge($pageSearch)->sortByDesc('score'); + if ($entityType !== 'all') { + $entityTypesToSearch = $entityType; + } else if (isset($terms['filters']['type'])) { + $entityTypesToSearch = explode('|', $terms['filters']['type']); + } + + // TODO - Check drafts don't show up in results + foreach ($entityTypesToSearch as $entityType) { + if (!in_array($entityType, $entityTypes)) continue; + $search = $this->searchEntityTable($terms, $entityType, $page, $count); + $results = $results->merge($search); + } + + return $results->sortByDesc('score'); } /** * Search across a particular entity type. - * @param string $searchString + * @param array $terms * @param string $entityType * @param int $page * @param int $count * @return \Illuminate\Database\Eloquent\Collection|static[] */ - public function searchEntityTable($searchString, $entityType = 'page', $page = 0, $count = 20) + public function searchEntityTable($terms, $entityType = 'page', $page = 0, $count = 20) { - $searchTerms = $this->parseSearchString($searchString); - $entity = $this->getEntity($entityType); $entitySelect = $entity->newQuery(); // Handle normal search terms - if (count($searchTerms['search']) > 0) { + if (count($terms['search']) > 0) { $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); - $subQuery->where(function(Builder $query) use ($searchTerms) { - foreach ($searchTerms['search'] as $inputTerm) { + $subQuery->where(function(Builder $query) use ($terms) { + foreach ($terms['search'] as $inputTerm) { $query->orWhere('term', 'like', $inputTerm .'%'); } })->groupBy('entity_type', 'entity_id'); @@ -99,9 +109,9 @@ class SearchService } // Handle exact term matching - if (count($searchTerms['exact']) > 0) { - $entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($searchTerms, $entity) { - foreach ($searchTerms['exact'] as $inputTerm) { + if (count($terms['exact']) > 0) { + $entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) { + foreach ($terms['exact'] as $inputTerm) { $query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) { $query->where('name', 'like', '%'.$inputTerm .'%') ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); @@ -111,16 +121,14 @@ class SearchService } // Handle tag searches - foreach ($searchTerms['tags'] as $inputTerm) { + foreach ($terms['tags'] as $inputTerm) { $this->applyTagSearch($entitySelect, $inputTerm); } // Handle filters - foreach ($searchTerms['filters'] as $filterTerm) { - $splitTerm = explode(':', $filterTerm); - $functionName = camel_case('filter_' . $splitTerm[0]); - $param = count($splitTerm) > 1 ? $splitTerm[1] : ''; - if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $param); + foreach ($terms['filters'] as $filterTerm => $filterValue) { + $functionName = camel_case('filter_' . $filterTerm); + if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue); } $entitySelect->skip($page * $count)->take($count); @@ -149,6 +157,7 @@ class SearchService 'filters' => '/\{(.*?)\}/' ]; + // Parse special terms foreach ($patterns as $termType => $pattern) { $matches = []; preg_match_all($pattern, $searchString, $matches); @@ -158,10 +167,19 @@ class SearchService } } + // Parse standard terms foreach (explode(' ', trim($searchString)) as $searchTerm) { if ($searchTerm !== '') $terms['search'][] = $searchTerm; } + // Split filter values out + $splitFilters = []; + foreach ($terms['filters'] as $filter) { + $explodedFilter = explode(':', $filter, 1); + $splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : ''; + } + $terms['filters'] = $splitFilters; + return $terms; } diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 9d789d9b4..000000000 --- a/gulpfile.js +++ /dev/null @@ -1,8 +0,0 @@ -var elixir = require('laravel-elixir'); - -elixir(mix => { - mix.sass('styles.scss'); - mix.sass('print-styles.scss'); - mix.sass('export-styles.scss'); - mix.browserify('global.js', './public/js/common.js'); -}); diff --git a/package.json b/package.json index b0805c918..1d7e8e268 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { "private": true, "scripts": { - "build": "gulp --production", - "dev": "gulp watch", - "watch": "gulp watch" + "dev": "npm run development", + "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch-poll": "npm run watch -- --watch-poll", + "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", + "prod": "npm run production", + "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" }, "devDependencies": { "angular": "^1.5.5", @@ -11,14 +15,16 @@ "angular-resource": "^1.5.5", "angular-sanitize": "^1.5.5", "angular-ui-sortable": "^0.15.0", + "cross-env": "^3.2.3", "dropzone": "^4.0.1", "gulp": "^3.9.0", - "laravel-elixir": "^6.0.0-11", - "laravel-elixir-browserify-official": "^0.1.3", + "laravel-mix": "0.*", "marked": "^0.3.5", "moment": "^2.12.0" }, "dependencies": { - "clipboard": "^1.5.16" + "axios": "^0.16.1", + "clipboard": "^1.5.16", + "vue": "^2.2.6" } } diff --git a/public/mix-manifest.json b/public/mix-manifest.json new file mode 100644 index 000000000..3885bcd54 --- /dev/null +++ b/public/mix-manifest.json @@ -0,0 +1,7 @@ +{ + "/js/common.js": "/js/common.js", + "/css/styles.css": "/css/styles.css", + "/css/print-styles.css": "/css/print-styles.css", + "/css/export-styles.css": "/css/export-styles.css", + "/js/vues.js": "/js/vues.js" +} \ No newline at end of file diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff b/resources/assets/fonts/roboto-mono-v4-latin-regular.woff similarity index 100% rename from public/fonts/roboto-mono-v4-latin-regular.woff rename to resources/assets/fonts/roboto-mono-v4-latin-regular.woff diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff2 b/resources/assets/fonts/roboto-mono-v4-latin-regular.woff2 similarity index 100% rename from public/fonts/roboto-mono-v4-latin-regular.woff2 rename to resources/assets/fonts/roboto-mono-v4-latin-regular.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-100.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-100.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-100italic.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-300.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-300.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-300italic.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-500.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-500.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-500italic.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-700.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-700.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-700italic.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-italic.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-italic.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff2 diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff b/resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-regular.woff rename to resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 b/resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff2 similarity index 100% rename from public/fonts/roboto-v15-cyrillic_latin-regular.woff2 rename to resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff2 diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 650919f85..7c980f6e9 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -1,12 +1,5 @@ "use strict"; -// AngularJS - Create application and load components -import angular from "angular"; -import "angular-resource"; -import "angular-animate"; -import "angular-sanitize"; -import "angular-ui-sortable"; - // Url retrieval function window.baseUrl = function(path) { let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content'); @@ -15,6 +8,28 @@ window.baseUrl = function(path) { return basePath + '/' + path; }; +// Vue and axios setup +import vue from "vue/dist/vue.common"; +import axios from "axios"; + +let axiosInstance = axios.create({ + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), + 'baseURL': baseUrl('') + } +}); + +window.Vue = vue; +window.axios = axiosInstance; +Vue.prototype.$http = axiosInstance; + +// AngularJS - Create application and load components +import angular from "angular"; +import "angular-resource"; +import "angular-animate"; +import "angular-sanitize"; +import "angular-ui-sortable"; + let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); // Translation setup @@ -47,6 +62,7 @@ class EventManager { } window.Events = new EventManager(); +Vue.prototype.$events = window.Events; // Load in angular specific items import Services from './services'; diff --git a/resources/assets/js/vues/search.js b/resources/assets/js/vues/search.js new file mode 100644 index 000000000..1fcd690c4 --- /dev/null +++ b/resources/assets/js/vues/search.js @@ -0,0 +1,66 @@ + +let termString = document.querySelector('[name=searchTerm]').value; +let terms = termString.split(' '); + +let data = { + terms: terms, + termString : termString, + search: { + type: { + page: true, + chapter: true, + book: true + } + } +}; + +let computed = { + +}; + +let methods = { + + appendTerm(term) { + if (this.termString.slice(-1) !== " ") this.termString += ' '; + this.termString += term; + }, + + typeParse(searchString) { + let typeFilter = /{\s?type:\s?(.*?)\s?}/; + let match = searchString.match(typeFilter); + let type = this.search.type; + if (!match) { + type.page = type.book = type.chapter = true; + return; + } + let splitTypes = match[1].replace(/ /g, '').split('|'); + type.page = (splitTypes.indexOf('page') !== -1); + type.chapter = (splitTypes.indexOf('chapter') !== -1); + type.book = (splitTypes.indexOf('book') !== -1); + }, + + typeChange() { + let typeFilter = /{\s?type:\s?(.*?)\s?}/; + let type = this.search.type; + if (type.page === type.chapter && type.page === type.book) { + this.termString = this.termString.replace(typeFilter, ''); + return; + } + let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|'); + let typeTerm = '{type:'+selectedTypes+'}'; + if (this.termString.match(typeFilter)) { + this.termString = this.termString.replace(typeFilter, typeTerm); + return; + } + this.appendTerm(typeTerm); + } + +}; + +function created() { + this.typeParse(this.termString); +} + +module.exports = { + data, computed, methods, created +}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js new file mode 100644 index 000000000..d50018598 --- /dev/null +++ b/resources/assets/js/vues/vues.js @@ -0,0 +1,16 @@ + +function exists(id) { + return document.getElementById(id) !== null; +} + +let vueMapping = { + 'search-system': require('./search') +}; + +Object.keys(vueMapping).forEach(id => { + if (exists(id)) { + let config = vueMapping[id]; + config.el = '#' + id; + new Vue(config); + } +}); \ No newline at end of file diff --git a/resources/assets/sass/_fonts.scss b/resources/assets/sass/_fonts.scss index c8e8ea833..7d19f051c 100644 --- a/resources/assets/sass/_fonts.scss +++ b/resources/assets/sass/_fonts.scss @@ -6,8 +6,8 @@ font-style: normal; font-weight: 100; src: local('Roboto Thin'), local('Roboto-Thin'), - url('../fonts/roboto-v15-cyrillic_latin-100.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-100.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-100.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-100.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-100italic - cyrillic_latin */ @font-face { @@ -15,8 +15,8 @@ font-style: italic; font-weight: 100; src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), - url('../fonts/roboto-v15-cyrillic_latin-100italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-100italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-100italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-100italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-300 - cyrillic_latin */ @font-face { @@ -24,8 +24,8 @@ font-style: normal; font-weight: 300; src: local('Roboto Light'), local('Roboto-Light'), - url('../fonts/roboto-v15-cyrillic_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-300italic - cyrillic_latin */ @font-face { @@ -33,8 +33,8 @@ font-style: italic; font-weight: 300; src: local('Roboto Light Italic'), local('Roboto-LightItalic'), - url('../fonts/roboto-v15-cyrillic_latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-regular - cyrillic_latin */ @font-face { @@ -42,8 +42,8 @@ font-style: normal; font-weight: 400; src: local('Roboto'), local('Roboto-Regular'), - url('../fonts/roboto-v15-cyrillic_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-italic - cyrillic_latin */ @font-face { @@ -51,8 +51,8 @@ font-style: italic; font-weight: 400; src: local('Roboto Italic'), local('Roboto-Italic'), - url('../fonts/roboto-v15-cyrillic_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-500 - cyrillic_latin */ @font-face { @@ -60,8 +60,8 @@ font-style: normal; font-weight: 500; src: local('Roboto Medium'), local('Roboto-Medium'), - url('../fonts/roboto-v15-cyrillic_latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-500italic - cyrillic_latin */ @font-face { @@ -69,8 +69,8 @@ font-style: italic; font-weight: 500; src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), - url('../fonts/roboto-v15-cyrillic_latin-500italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-500italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-500italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-500italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-700 - cyrillic_latin */ @font-face { @@ -78,8 +78,8 @@ font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), - url('../fonts/roboto-v15-cyrillic_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-700italic - cyrillic_latin */ @font-face { @@ -87,8 +87,8 @@ font-style: italic; font-weight: 700; src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), - url('../fonts/roboto-v15-cyrillic_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-v15-cyrillic_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-v15-cyrillic_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-mono-regular - latin */ @@ -97,6 +97,6 @@ font-style: normal; font-weight: 400; src: local('Roboto Mono'), local('RobotoMono-Regular'), - url('../fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('../fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('assets/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('assets/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 95a9d72b0..2251ed2df 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -84,6 +84,7 @@
@yield('bottom') + @yield('scripts') diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index 54626daf1..eb8ef51f3 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -2,6 +2,9 @@ @section('content') + + +
@@ -15,40 +18,41 @@
-
+

{{ trans('entities.search_results') }}

-

- {{--TODO - Remove these pages--}} - Remove these links (Commented out) - {{--@if(count($pages) > 0)--}} - {{--{{ trans('entities.search_view_pages') }}--}} - {{--@endif--}} - - {{--@if(count($chapters) > 0)--}} - {{--    --}} - {{--{{ trans('entities.search_view_chapters') }}--}} - {{--@endif--}} - - {{--@if(count($books) > 0)--}} - {{--    --}} - {{--{{ trans('entities.search_view_books') }}--}} - {{--@endif--}} -

+
+
-

{{ trans('entities.pages') }}

- @include('partials/entity-list', ['entities' => $entities, 'style' => 'detailed']) + @include('partials/entity-list', ['entities' => $entities])
+
- Sidebar filter controls +

Search Filters

+ +

Content Type

+
+ + + +
+
+
+
+ +@stop + +@section('scripts') + @stop \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index f5ee3f827..dad7a55e5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -123,10 +123,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/link/{id}', 'PageController@redirectFromLink'); // Search - Route::get('/search', 'SearchController@searchAll'); - Route::get('/search/pages', 'SearchController@searchPages'); - Route::get('/search/books', 'SearchController@searchBooks'); - Route::get('/search/chapters', 'SearchController@searchChapters'); + Route::get('/search', 'SearchController@search'); Route::get('/search/book/{bookId}', 'SearchController@searchBook'); // Other Pages diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 000000000..2e691bd50 --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,18 @@ +const { mix } = require('laravel-mix'); + +/* + |-------------------------------------------------------------------------- + | Mix Asset Management + |-------------------------------------------------------------------------- + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your Laravel application. By default, we are compiling the Sass + | file for the application as well as bundling up all the JS files. + | + */ + +mix.js('resources/assets/js/global.js', './public/js/common.js') + .js('resources/assets/js/vues/vues.js', './public/js/vues.js') + .sass('resources/assets/sass/styles.scss', 'public/css') + .sass('resources/assets/sass/print-styles.scss', 'public/css') + .sass('resources/assets/sass/export-styles.scss', 'public/css'); From 46f3d78c8a9b8aa25c43d2449eb78e949d1dcc7e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 9 Apr 2017 21:12:13 +0100 Subject: [PATCH 08/14] Fixed entity type filter bug in new search system --- app/Services/SearchService.php | 2 +- resources/assets/js/vues/search.js | 4 ++++ resources/views/search/all.blade.php | 10 +++------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 8202b4997..a2844c593 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -175,7 +175,7 @@ class SearchService // Split filter values out $splitFilters = []; foreach ($terms['filters'] as $filter) { - $explodedFilter = explode(':', $filter, 1); + $explodedFilter = explode(':', $filter, 2); $splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : ''; } $terms['filters'] = $splitFilters; diff --git a/resources/assets/js/vues/search.js b/resources/assets/js/vues/search.js index 1fcd690c4..708418271 100644 --- a/resources/assets/js/vues/search.js +++ b/resources/assets/js/vues/search.js @@ -53,6 +53,10 @@ let methods = { return; } this.appendTerm(typeTerm); + }, + + updateSearch() { + window.location = '/search?term=' + encodeURIComponent(this.termString); } }; diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index eb8ef51f3..ac5cd7db4 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -40,6 +40,9 @@
+ + +
@@ -48,11 +51,4 @@ -@stop - -@section('scripts') - @stop \ No newline at end of file From ad125327c040830596736f9ac48f97763836318d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 14 Apr 2017 18:47:33 +0100 Subject: [PATCH 09/14] Migrated to custom gulp setup and conintue search interface --- .gitignore | 1 - gulpfile.js | 63 ++++++++++ package.json | 45 ++++--- .../fonts/roboto-mono-v4-latin-regular.woff | Bin .../fonts/roboto-mono-v4-latin-regular.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-100.woff | Bin .../fonts/roboto-v15-cyrillic_latin-100.woff2 | Bin .../roboto-v15-cyrillic_latin-100italic.woff | Bin .../roboto-v15-cyrillic_latin-100italic.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-300.woff | Bin .../fonts/roboto-v15-cyrillic_latin-300.woff2 | Bin .../roboto-v15-cyrillic_latin-300italic.woff | Bin .../roboto-v15-cyrillic_latin-300italic.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-500.woff | Bin .../fonts/roboto-v15-cyrillic_latin-500.woff2 | Bin .../roboto-v15-cyrillic_latin-500italic.woff | Bin .../roboto-v15-cyrillic_latin-500italic.woff2 | Bin .../fonts/roboto-v15-cyrillic_latin-700.woff | Bin .../fonts/roboto-v15-cyrillic_latin-700.woff2 | Bin .../roboto-v15-cyrillic_latin-700italic.woff | Bin .../roboto-v15-cyrillic_latin-700italic.woff2 | Bin .../roboto-v15-cyrillic_latin-italic.woff | Bin .../roboto-v15-cyrillic_latin-italic.woff2 | Bin .../roboto-v15-cyrillic_latin-regular.woff | Bin .../roboto-v15-cyrillic_latin-regular.woff2 | Bin public/mix-manifest.json | 7 -- resources/assets/js/controllers.js | 8 +- resources/assets/js/directives.js | 6 +- resources/assets/js/global.js | 32 ++--- resources/assets/js/pages/page-form.js | 4 +- resources/assets/js/pages/page-show.js | 6 +- resources/assets/js/translations.js | 2 +- resources/assets/js/vues/search.js | 116 ++++++++++++++++-- resources/assets/js/vues/vues.js | 1 + resources/assets/sass/_fonts.scss | 44 +++---- resources/views/base.blade.php | 1 - resources/views/search/all.blade.php | 93 ++++++++++++-- webpack.mix.js | 18 --- 38 files changed, 337 insertions(+), 110 deletions(-) create mode 100644 gulpfile.js rename {resources/assets => public}/fonts/roboto-mono-v4-latin-regular.woff (100%) rename {resources/assets => public}/fonts/roboto-mono-v4-latin-regular.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-100.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-100.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-100italic.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-100italic.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-300.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-300.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-300italic.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-300italic.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-500.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-500.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-500italic.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-500italic.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-700.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-700.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-700italic.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-700italic.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-italic.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-italic.woff2 (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-regular.woff (100%) rename {resources/assets => public}/fonts/roboto-v15-cyrillic_latin-regular.woff2 (100%) delete mode 100644 public/mix-manifest.json delete mode 100644 webpack.mix.js diff --git a/.gitignore b/.gitignore index 856cf3722..5f41a864e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ Homestead.yaml /public/plugins /public/css /public/js -/public/fonts /public/bower /storage/images _ide_helper.php diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..b72bb366d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,63 @@ +const argv = require('yargs').argv; +const gulp = require('gulp'), + plumber = require('gulp-plumber'); +const autoprefixer = require('gulp-autoprefixer'); +const uglify = require('gulp-uglify'); +const minifycss = require('gulp-clean-css'); +const sass = require('gulp-sass'); +const browserify = require("browserify"); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const babelify = require("babelify"); +const watchify = require("watchify"); +const envify = require("envify"); +const gutil = require("gulp-util"); + +if (argv.production) process.env.NODE_ENV = 'production'; + +gulp.task('styles', () => { + let chain = gulp.src(['resources/assets/sass/**/*.scss']) + .pipe(plumber({ + errorHandler: function (error) { + console.log(error.message); + this.emit('end'); + }})) + .pipe(sass()) + .pipe(autoprefixer('last 2 versions')); + if (argv.production) chain = chain.pipe(minifycss()); + return chain.pipe(gulp.dest('public/css/')); +}); + + +function scriptTask(watch=false) { + + let props = { + basedir: 'resources/assets/js', + debug: true, + entries: ['global.js'] + }; + + let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props); + bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']}); + function rebundle() { + let stream = bundler.bundle(); + stream = stream.pipe(source('common.js')); + if (argv.production) stream = stream.pipe(buffer()).pipe(uglify()); + return stream.pipe(gulp.dest('public/js/')); + } + bundler.on('update', function() { + rebundle(); + gutil.log('Rebundle...'); + }); + bundler.on('log', gutil.log); + return rebundle(); +} + +gulp.task('scripts', () => {scriptTask(false)}); +gulp.task('scripts-watch', () => {scriptTask(true)}); + +gulp.task('default', ['styles', 'scripts-watch'], () => { + gulp.watch("resources/assets/sass/**/*.scss", ['styles']); +}); + +gulp.task('build', ['styles', 'scripts']); \ No newline at end of file diff --git a/package.json b/package.json index 1d7e8e268..9f2ce4c1a 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,43 @@ { "private": true, "scripts": { - "dev": "npm run development", - "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", - "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", - "watch-poll": "npm run watch -- --watch-poll", - "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", - "prod": "npm run production", - "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" + "build": "gulp build", + "production": "gulp build --production", + "dev": "gulp", + "watch": "gulp" }, "devDependencies": { + "babelify": "^7.3.0", + "browserify": "^14.3.0", + "envify": "^4.0.0", + "gulp": "3.9.1", + "gulp-autoprefixer": "3.1.1", + "gulp-clean-css": "^3.0.4", + "gulp-minify-css": "1.2.4", + "gulp-plumber": "1.1.0", + "gulp-sass": "3.1.0", + "gulp-uglify": "2.1.2", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0", + "watchify": "^3.9.0", + "yargs": "^7.1.0" + }, + "dependencies": { "angular": "^1.5.5", "angular-animate": "^1.5.5", "angular-resource": "^1.5.5", "angular-sanitize": "^1.5.5", - "angular-ui-sortable": "^0.15.0", - "cross-env": "^3.2.3", - "dropzone": "^4.0.1", - "gulp": "^3.9.0", - "laravel-mix": "0.*", - "marked": "^0.3.5", - "moment": "^2.12.0" - }, - "dependencies": { + "angular-ui-sortable": "^0.17.0", "axios": "^0.16.1", + "babel-preset-es2015": "^6.24.1", "clipboard": "^1.5.16", + "dropzone": "^4.0.1", + "gulp-util": "^3.0.8", + "marked": "^0.3.5", + "moment": "^2.12.0", "vue": "^2.2.6" + }, + "browser": { + "vue": "vue/dist/vue.common.js" } } diff --git a/resources/assets/fonts/roboto-mono-v4-latin-regular.woff b/public/fonts/roboto-mono-v4-latin-regular.woff similarity index 100% rename from resources/assets/fonts/roboto-mono-v4-latin-regular.woff rename to public/fonts/roboto-mono-v4-latin-regular.woff diff --git a/resources/assets/fonts/roboto-mono-v4-latin-regular.woff2 b/public/fonts/roboto-mono-v4-latin-regular.woff2 similarity index 100% rename from resources/assets/fonts/roboto-mono-v4-latin-regular.woff2 rename to public/fonts/roboto-mono-v4-latin-regular.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff b/public/fonts/roboto-v15-cyrillic_latin-100.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff rename to public/fonts/roboto-v15-cyrillic_latin-100.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-100.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-100.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff rename to public/fonts/roboto-v15-cyrillic_latin-100italic.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-100italic.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff b/public/fonts/roboto-v15-cyrillic_latin-300.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff rename to public/fonts/roboto-v15-cyrillic_latin-300.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-300.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-300.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff rename to public/fonts/roboto-v15-cyrillic_latin-300italic.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-300italic.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff b/public/fonts/roboto-v15-cyrillic_latin-500.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff rename to public/fonts/roboto-v15-cyrillic_latin-500.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-500.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-500.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff rename to public/fonts/roboto-v15-cyrillic_latin-500italic.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-500italic.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff b/public/fonts/roboto-v15-cyrillic_latin-700.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff rename to public/fonts/roboto-v15-cyrillic_latin-700.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-700.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-700.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff rename to public/fonts/roboto-v15-cyrillic_latin-700italic.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-700italic.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff b/public/fonts/roboto-v15-cyrillic_latin-italic.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff rename to public/fonts/roboto-v15-cyrillic_latin-italic.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-italic.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-italic.woff2 diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff b/public/fonts/roboto-v15-cyrillic_latin-regular.woff similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff rename to public/fonts/roboto-v15-cyrillic_latin-regular.woff diff --git a/resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff2 b/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 similarity index 100% rename from resources/assets/fonts/roboto-v15-cyrillic_latin-regular.woff2 rename to public/fonts/roboto-v15-cyrillic_latin-regular.woff2 diff --git a/public/mix-manifest.json b/public/mix-manifest.json deleted file mode 100644 index 3885bcd54..000000000 --- a/public/mix-manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "/js/common.js": "/js/common.js", - "/css/styles.css": "/css/styles.css", - "/css/print-styles.css": "/css/print-styles.css", - "/css/export-styles.css": "/css/export-styles.css", - "/js/vues.js": "/js/vues.js" -} \ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 0d57b09ad..c5baecf16 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -1,12 +1,12 @@ "use strict"; -import moment from 'moment'; -import 'moment/locale/en-gb'; -import editorOptions from "./pages/page-form"; +const moment = require('moment'); +require('moment/locale/en-gb'); +const editorOptions = require("./pages/page-form"); moment.locale('en-gb'); -export default function (ngApp, events) { +module.exports = function (ngApp, events) { ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', function ($scope, $attrs, $http, $timeout, imageManagerService) { diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 10458e753..19badcac8 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,8 +1,8 @@ "use strict"; -import DropZone from "dropzone"; -import markdown from "marked"; +const DropZone = require("dropzone"); +const markdown = require("marked"); -export default function (ngApp, events) { +module.exports = function (ngApp, events) { /** * Common tab controls using simple jQuery functions. diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 7c980f6e9..dc6802e12 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -8,33 +8,33 @@ window.baseUrl = function(path) { return basePath + '/' + path; }; -// Vue and axios setup -import vue from "vue/dist/vue.common"; -import axios from "axios"; +const Vue = require("vue"); +const axios = require("axios"); let axiosInstance = axios.create({ headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), - 'baseURL': baseUrl('') + 'baseURL': window.baseUrl('') } }); -window.Vue = vue; -window.axios = axiosInstance; Vue.prototype.$http = axiosInstance; +require("./vues/vues"); + + // AngularJS - Create application and load components -import angular from "angular"; -import "angular-resource"; -import "angular-animate"; -import "angular-sanitize"; -import "angular-ui-sortable"; +const angular = require("angular"); +require("angular-resource"); +require("angular-animate"); +require("angular-sanitize"); +require("angular-ui-sortable"); let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); // Translation setup // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system -import Translations from "./translations" +const Translations = require("./translations"); let translator = new Translations(window.translations); window.trans = translator.get.bind(translator); @@ -65,9 +65,9 @@ window.Events = new EventManager(); Vue.prototype.$events = window.Events; // Load in angular specific items -import Services from './services'; -import Directives from './directives'; -import Controllers from './controllers'; +const Services = require('./services'); +const Directives = require('./directives'); +const Controllers = require('./controllers'); Services(ngApp, window.Events); Directives(ngApp, window.Events); Controllers(ngApp, window.Events); @@ -170,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1 } // Page specific items -import "./pages/page-show"; +require("./pages/page-show"); diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 0f44b3d09..b5a0a2998 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) { editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']); } -export default function() { +module.exports = function() { let settings = { selector: '#html-editor', content_css: [ @@ -213,4 +213,4 @@ export default function() { } }; return settings; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 0f45e1987..cc6296434 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -1,8 +1,8 @@ "use strict"; // Configure ZeroClipboard -import Clipboard from "clipboard"; +const Clipboard = require("clipboard"); -export default window.setupPageShow = function (pageId) { +let setupPageShow = window.setupPageShow = function (pageId) { // Set up pointer let $pointer = $('#pointer').detach(); @@ -151,3 +151,5 @@ export default window.setupPageShow = function (pageId) { }); }; + +module.exports = setupPageShow; \ No newline at end of file diff --git a/resources/assets/js/translations.js b/resources/assets/js/translations.js index 306c696b6..ca6a7bd29 100644 --- a/resources/assets/js/translations.js +++ b/resources/assets/js/translations.js @@ -44,4 +44,4 @@ class Translator { } -export default Translator +module.exports = Translator; diff --git a/resources/assets/js/vues/search.js b/resources/assets/js/vues/search.js index 708418271..710f4b214 100644 --- a/resources/assets/js/vues/search.js +++ b/resources/assets/js/vues/search.js @@ -1,16 +1,18 @@ - -let termString = document.querySelector('[name=searchTerm]').value; -let terms = termString.split(' '); +const moment = require('moment'); let data = { - terms: terms, - termString : termString, - search: { + terms: '', + termString : '', + search: { type: { page: true, chapter: true, book: true - } + }, + exactTerms: [], + tagTerms: [], + option: {}, + dates: {} } }; @@ -21,8 +23,76 @@ let computed = { let methods = { appendTerm(term) { - if (this.termString.slice(-1) !== " ") this.termString += ' '; - this.termString += term; + this.termString += ' ' + term; + this.termString = this.termString.replace(/\s{2,}/g, ' '); + this.termString = this.termString.replace(/^\s+/, ''); + this.termString = this.termString.replace(/\s+$/, ''); + }, + + exactParse(searchString) { + this.search.exactTerms = []; + let exactFilter = /"(.+?)"/g; + let matches; + while ((matches = exactFilter.exec(searchString)) !== null) { + this.search.exactTerms.push(matches[1]); + } + }, + + exactChange() { + let exactFilter = /"(.+?)"/g; + this.termString = this.termString.replace(exactFilter, ''); + let matchesTerm = this.search.exactTerms.filter(term => { + return term.trim() !== ''; + }).map(term => { + return `"${term}"` + }).join(' '); + this.appendTerm(matchesTerm); + }, + + addExact() { + this.search.exactTerms.push(''); + setTimeout(() => { + let exactInputs = document.querySelectorAll('.exact-input'); + exactInputs[exactInputs.length - 1].focus(); + }, 100); + }, + + removeExact(index) { + this.search.exactTerms.splice(index, 1); + this.exactChange(); + }, + + tagParse(searchString) { + this.search.tagTerms = []; + let tagFilter = /\[(.+?)\]/g; + let matches; + while ((matches = tagFilter.exec(searchString)) !== null) { + this.search.tagTerms.push(matches[1]); + } + }, + + tagChange() { + let tagFilter = /\[(.+?)\]/g; + this.termString = this.termString.replace(tagFilter, ''); + let matchesTerm = this.search.tagTerms.filter(term => { + return term.trim() !== ''; + }).map(term => { + return `[${term}]` + }).join(' '); + this.appendTerm(matchesTerm); + }, + + addTag() { + this.search.tagTerms.push(''); + setTimeout(() => { + let tagInputs = document.querySelectorAll('.tag-input'); + tagInputs[tagInputs.length - 1].focus(); + }, 100); + }, + + removeTag(index) { + this.search.tagTerms.splice(index, 1); + this.tagChange(); }, typeParse(searchString) { @@ -55,14 +125,40 @@ let methods = { this.appendTerm(typeTerm); }, - updateSearch() { + optionParse(searchString) { + let optionFilter = /{([a-z_-]+?)}/gi; + let matches; + while ((matches = optionFilter.exec(searchString)) !== null) { + this.search.option[matches[1].toLowerCase()] = true; + } + }, + + optionChange(optionName) { + let isChecked = this.search.option[optionName]; + if (isChecked) { + this.appendTerm(`{${optionName}}`); + } else { + this.termString = this.termString.replace(`{${optionName}}`, ''); + } + }, + + updateSearch(e) { + e.preventDefault(); window.location = '/search?term=' + encodeURIComponent(this.termString); + }, + + enableDate(optionName) { + this.search.dates[optionName] = moment().format('YYYY-MM-DD'); } }; function created() { + this.termString = document.querySelector('[name=searchTerm]').value; this.typeParse(this.termString); + this.exactParse(this.termString); + this.tagParse(this.termString); + this.optionParse(this.termString); } module.exports = { diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index d50018598..832a5415d 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -1,3 +1,4 @@ +const Vue = require("vue"); function exists(id) { return document.getElementById(id) !== null; diff --git a/resources/assets/sass/_fonts.scss b/resources/assets/sass/_fonts.scss index 7d19f051c..c8e8ea833 100644 --- a/resources/assets/sass/_fonts.scss +++ b/resources/assets/sass/_fonts.scss @@ -6,8 +6,8 @@ font-style: normal; font-weight: 100; src: local('Roboto Thin'), local('Roboto-Thin'), - url('assets/fonts/roboto-v15-cyrillic_latin-100.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-100.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-100.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-100.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-100italic - cyrillic_latin */ @font-face { @@ -15,8 +15,8 @@ font-style: italic; font-weight: 100; src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), - url('assets/fonts/roboto-v15-cyrillic_latin-100italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-100italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-100italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-100italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-300 - cyrillic_latin */ @font-face { @@ -24,8 +24,8 @@ font-style: normal; font-weight: 300; src: local('Roboto Light'), local('Roboto-Light'), - url('assets/fonts/roboto-v15-cyrillic_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-300italic - cyrillic_latin */ @font-face { @@ -33,8 +33,8 @@ font-style: italic; font-weight: 300; src: local('Roboto Light Italic'), local('Roboto-LightItalic'), - url('assets/fonts/roboto-v15-cyrillic_latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-regular - cyrillic_latin */ @font-face { @@ -42,8 +42,8 @@ font-style: normal; font-weight: 400; src: local('Roboto'), local('Roboto-Regular'), - url('assets/fonts/roboto-v15-cyrillic_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-italic - cyrillic_latin */ @font-face { @@ -51,8 +51,8 @@ font-style: italic; font-weight: 400; src: local('Roboto Italic'), local('Roboto-Italic'), - url('assets/fonts/roboto-v15-cyrillic_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-500 - cyrillic_latin */ @font-face { @@ -60,8 +60,8 @@ font-style: normal; font-weight: 500; src: local('Roboto Medium'), local('Roboto-Medium'), - url('assets/fonts/roboto-v15-cyrillic_latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-500italic - cyrillic_latin */ @font-face { @@ -69,8 +69,8 @@ font-style: italic; font-weight: 500; src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), - url('assets/fonts/roboto-v15-cyrillic_latin-500italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-500italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-500italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-500italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-700 - cyrillic_latin */ @font-face { @@ -78,8 +78,8 @@ font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), - url('assets/fonts/roboto-v15-cyrillic_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-700italic - cyrillic_latin */ @font-face { @@ -87,8 +87,8 @@ font-style: italic; font-weight: 700; src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), - url('assets/fonts/roboto-v15-cyrillic_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-v15-cyrillic_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-v15-cyrillic_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-v15-cyrillic_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* roboto-mono-regular - latin */ @@ -97,6 +97,6 @@ font-style: normal; font-weight: 400; src: local('Roboto Mono'), local('RobotoMono-Regular'), - url('assets/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ - url('assets/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('../fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('../fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 2251ed2df..95a9d72b0 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -84,7 +84,6 @@ @yield('bottom') - @yield('scripts') diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index ac5cd7db4..d1f928912 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -33,15 +33,94 @@

Search Filters

-

Content Type

-
- - - -
+
+

Content Type

+
+ + + +
+ +

Exact Matches

+ + + + + + + + +
+ + +
+ +
+ +

Tag Searches

+ + + + + + + + +
+ + +
+ +
+ +

Options

+ + + +

Date Options

+ + + + + + + + + +
Updated After + + + + +
+ +
+ + + +
-
diff --git a/webpack.mix.js b/webpack.mix.js deleted file mode 100644 index 2e691bd50..000000000 --- a/webpack.mix.js +++ /dev/null @@ -1,18 +0,0 @@ -const { mix } = require('laravel-mix'); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Laravel application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix.js('resources/assets/js/global.js', './public/js/common.js') - .js('resources/assets/js/vues/vues.js', './public/js/vues.js') - .sass('resources/assets/sass/styles.scss', 'public/css') - .sass('resources/assets/sass/print-styles.scss', 'public/css') - .sass('resources/assets/sass/export-styles.scss', 'public/css'); From 0e0945ef8465adaf1a195d729c4bdc9e8c37de60 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Apr 2017 15:04:30 +0100 Subject: [PATCH 10/14] Finished off UI for search system --- app/Http/Controllers/SearchController.php | 14 +- app/Services/SearchService.php | 30 ++-- resources/assets/js/vues/search.js | 35 ++++- resources/assets/sass/_animations.scss | 2 +- resources/assets/sass/_forms.scss | 19 ++- resources/assets/sass/styles.scss | 13 +- resources/lang/de/entities.php | 9 -- resources/lang/en/common.php | 1 + resources/lang/en/entities.php | 26 ++-- resources/lang/es/entities.php | 9 -- resources/lang/fr/entities.php | 9 -- resources/lang/nl/entities.php | 9 -- resources/lang/pt_BR/entities.php | 9 -- resources/views/search/all.blade.php | 160 ++++++++++++++++------ 14 files changed, 225 insertions(+), 120 deletions(-) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 6d29ab17b..b65bca51e 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -34,14 +34,20 @@ class SearchController extends Controller public function search(Request $request) { $searchTerm = $request->get('term'); -// $paginationAppends = $request->only('term'); TODO - Check pagination $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); - $entities = $this->searchService->searchEntities($searchTerm); + $page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1; + $nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1)); + + $results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20); + $hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0; return view('search/all', [ - 'entities' => $entities, - 'searchTerm' => $searchTerm + 'entities' => $results['results'], + 'totalResults' => $results['total'], + 'searchTerm' => $searchTerm, + 'hasNextPage' => $hasNextPage, + 'nextPageLink' => $nextPageLink ]); } diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index a2844c593..7ecfb95c7 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -8,7 +8,6 @@ use BookStack\SearchTerm; use Illuminate\Database\Connection; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; -use Illuminate\Support\Collection; class SearchService { @@ -56,9 +55,9 @@ class SearchService * @param string $entityType * @param int $page * @param int $count - * @return Collection + * @return array[int, Collection]; */ - public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) + public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20) { $terms = $this->parseSearchString($searchString); $entityTypes = array_keys($this->entities); @@ -71,14 +70,20 @@ class SearchService $entityTypesToSearch = explode('|', $terms['filters']['type']); } - // TODO - Check drafts don't show up in results + $total = 0; + foreach ($entityTypesToSearch as $entityType) { if (!in_array($entityType, $entityTypes)) continue; $search = $this->searchEntityTable($terms, $entityType, $page, $count); + $total += $this->searchEntityTable($terms, $entityType, $page, $count, true); $results = $results->merge($search); } - return $results->sortByDesc('score'); + return [ + 'total' => $total, + 'count' => count($results), + 'results' => $results->sortByDesc('score') + ]; } /** @@ -87,9 +92,10 @@ class SearchService * @param string $entityType * @param int $page * @param int $count - * @return \Illuminate\Database\Eloquent\Collection|static[] + * @param bool $getCount Return the total count of the search + * @return \Illuminate\Database\Eloquent\Collection|int|static[] */ - public function searchEntityTable($terms, $entityType = 'page', $page = 0, $count = 20) + public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false) { $entity = $this->getEntity($entityType); $entitySelect = $entity->newQuery(); @@ -131,8 +137,10 @@ class SearchService if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue); } - $entitySelect->skip($page * $count)->take($count); $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); + if ($getCount) return $query->count(); + + $query = $query->skip(($page-1) * $count)->take($count); return $query->get(); } @@ -371,13 +379,15 @@ class SearchService protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - if (!is_numeric($input)) return; + if (!is_numeric($input) && $input !== 'me') return; + if ($input === 'me') $input = user()->id; $query->where('created_by', '=', $input); } protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - if (!is_numeric($input)) return; + if (!is_numeric($input) && $input !== 'me') return; + if ($input === 'me') $input = user()->id; $query->where('updated_by', '=', $input); } diff --git a/resources/assets/js/vues/search.js b/resources/assets/js/vues/search.js index 710f4b214..515ca3bc9 100644 --- a/resources/assets/js/vues/search.js +++ b/resources/assets/js/vues/search.js @@ -12,7 +12,12 @@ let data = { exactTerms: [], tagTerms: [], option: {}, - dates: {} + dates: { + updated_after: false, + updated_before: false, + created_after: false, + created_before: false, + } } }; @@ -126,7 +131,7 @@ let methods = { }, optionParse(searchString) { - let optionFilter = /{([a-z_-]+?)}/gi; + let optionFilter = /{([a-z_\-:]+?)}/gi; let matches; while ((matches = optionFilter.exec(searchString)) !== null) { this.search.option[matches[1].toLowerCase()] = true; @@ -148,7 +153,30 @@ let methods = { }, enableDate(optionName) { - this.search.dates[optionName] = moment().format('YYYY-MM-DD'); + this.search.dates[optionName.toLowerCase()] = moment().format('YYYY-MM-DD'); + this.dateChange(optionName); + }, + + dateParse(searchString) { + let dateFilter = /{([a-z_\-]+?):([a-z_\-0-9]+?)}/gi; + let dateTags = Object.keys(this.search.dates); + let matches; + while ((matches = dateFilter.exec(searchString)) !== null) { + if (dateTags.indexOf(matches[1]) === -1) continue; + this.search.dates[matches[1].toLowerCase()] = matches[2]; + } + }, + + dateChange(optionName) { + let dateFilter = new RegExp('{\\s?'+optionName+'\\s?:([a-z_\\-0-9]+?)}', 'gi'); + this.termString = this.termString.replace(dateFilter, ''); + if (!this.search.dates[optionName]) return; + this.appendTerm(`{${optionName}:${this.search.dates[optionName]}}`); + }, + + dateRemove(optionName) { + this.search.dates[optionName] = false; + this.dateChange(optionName); } }; @@ -159,6 +187,7 @@ function created() { this.exactParse(this.termString); this.tagParse(this.termString); this.optionParse(this.termString); + this.dateParse(this.termString); } module.exports = { diff --git a/resources/assets/sass/_animations.scss b/resources/assets/sass/_animations.scss index 582d718c8..afcf01cff 100644 --- a/resources/assets/sass/_animations.scss +++ b/resources/assets/sass/_animations.scss @@ -2,7 +2,7 @@ .anim.fadeIn { opacity: 0; animation-name: fadeIn; - animation-duration: 160ms; + animation-duration: 180ms; animation-timing-function: ease-in-out; animation-fill-mode: forwards; } diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 7e6b800d2..1fc812896 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -98,19 +98,36 @@ label { label.radio, label.checkbox { font-weight: 400; + user-select: none; input[type="radio"], input[type="checkbox"] { margin-right: $-xs; } } +label.inline.checkbox { + margin-right: $-m; +} + label + p.small { margin-bottom: 0.8em; } -input[type="text"], input[type="number"], input[type="email"], input[type="search"], input[type="url"], input[type="password"], select, textarea { +table.form-table { + max-width: 100%; + td { + overflow: hidden; + padding: $-xxs/2 0; + } +} + +input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea { @extend .input-base; } +input[type=date] { + width: 190px; +} + .toggle-switch { display: inline-block; background-color: #BBB; diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 967aba76b..50c3a50b2 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -7,8 +7,8 @@ @import "grid"; @import "blocks"; @import "buttons"; -@import "forms"; @import "tables"; +@import "forms"; @import "animations"; @import "tinymce"; @import "highlightjs"; @@ -17,7 +17,11 @@ @import "lists"; @import "pages"; -[v-cloak], [v-show] {display: none;} +[v-cloak], [v-show] { + display: none; opacity: 0; + animation-name: none !important; +} + [ng\:cloak], [ng-cloak], .ng-cloak { display: none !important; @@ -272,8 +276,3 @@ $btt-size: 40px; - - - - - diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php index 2859e4ec5..c9feb8497 100644 --- a/resources/lang/de/entities.php +++ b/resources/lang/de/entities.php @@ -43,18 +43,9 @@ return [ * Search */ 'search_results' => 'Suchergebnisse', - 'search_results_page' => 'Seiten-Suchergebnisse', - 'search_results_chapter' => 'Kapitel-Suchergebnisse', - 'search_results_book' => 'Buch-Suchergebnisse', 'search_clear' => 'Suche zurücksetzen', - 'search_view_pages' => 'Zeige alle passenden Seiten', - 'search_view_chapters' => 'Zeige alle passenden Kapitel', - 'search_view_books' => 'Zeige alle passenden Bücher', 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden', 'search_for_term' => 'Suche nach :term', - 'search_page_for_term' => 'Suche nach :term in Seiten', - 'search_chapter_for_term' => 'Suche nach :term in Kapiteln', - 'search_book_for_term' => 'Suche nach :term in Büchern', /** * Books diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 31ef42e97..e1d74c95e 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -33,6 +33,7 @@ return [ 'search_clear' => 'Clear Search', 'reset' => 'Reset', 'remove' => 'Remove', + 'add' => 'Add', /** diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index f54134718..66c2e8042 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -43,18 +43,26 @@ return [ * Search */ 'search_results' => 'Search Results', - 'search_results_page' => 'Page Search Results', - 'search_results_chapter' => 'Chapter Search Results', - 'search_results_book' => 'Book Search Results', + 'search_total_results_found' => ':count result found|:count total results found', 'search_clear' => 'Clear Search', - 'search_view_pages' => 'View all matches pages', - 'search_view_chapters' => 'View all matches chapters', - 'search_view_books' => 'View all matches books', 'search_no_pages' => 'No pages matched this search', 'search_for_term' => 'Search for :term', - 'search_page_for_term' => 'Page search for :term', - 'search_chapter_for_term' => 'Chapter search for :term', - 'search_book_for_term' => 'Books search for :term', + 'search_more' => 'More Results', + 'search_filters' => 'Search Filters', + 'search_content_type' => 'Content Type', + 'search_exact_matches' => 'Exact Matches', + 'search_tags' => 'Tag Searches', + 'search_viewed_by_me' => 'Viewed by me', + 'search_not_viewed_by_me' => 'Not viewed by me', + 'search_permissions_set' => 'Permissions set', + 'search_created_by_me' => 'Created by me', + 'search_updated_by_me' => 'Updated by me', + 'search_updated_before' => 'Updated before', + 'search_updated_after' => 'Updated after', + 'search_created_before' => 'Created before', + 'search_created_after' => 'Created after', + 'search_set_date' => 'Set Date', + 'search_update' => 'Update Search', /** * Books diff --git a/resources/lang/es/entities.php b/resources/lang/es/entities.php index 14e952f1a..b03366da6 100644 --- a/resources/lang/es/entities.php +++ b/resources/lang/es/entities.php @@ -43,18 +43,9 @@ return [ * Search */ 'search_results' => 'Buscar resultados', - 'search_results_page' => 'resultados de búsqueda en página', - 'search_results_chapter' => 'Resultados de búsqueda en capítulo ', - 'search_results_book' => 'Resultados de búsqueda en libro', 'search_clear' => 'Limpiar resultados', - 'search_view_pages' => 'Ver todas las páginas que concuerdan', - 'search_view_chapters' => 'Ver todos los capítulos que concuerdan', - 'search_view_books' => 'Ver todos los libros que concuerdan', 'search_no_pages' => 'Ninguna página encontrada para la búsqueda', 'search_for_term' => 'Busqueda por :term', - 'search_page_for_term' => 'Búsqueda de página por :term', - 'search_chapter_for_term' => 'Búsqueda por capítulo de :term', - 'search_book_for_term' => 'Búsqueda en libro de :term', /** * Books diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php index cfd206b91..5562fb0fd 100644 --- a/resources/lang/fr/entities.php +++ b/resources/lang/fr/entities.php @@ -43,18 +43,9 @@ return [ * Search */ 'search_results' => 'Résultats de recherche', - 'search_results_page' => 'Résultats de recherche des pages', - 'search_results_chapter' => 'Résultats de recherche des chapitres', - 'search_results_book' => 'Résultats de recherche des livres', 'search_clear' => 'Réinitialiser la recherche', - 'search_view_pages' => 'Voir toutes les pages correspondantes', - 'search_view_chapters' => 'Voir tous les chapitres correspondants', - 'search_view_books' => 'Voir tous les livres correspondants', 'search_no_pages' => 'Aucune page correspondant à cette recherche', 'search_for_term' => 'recherche pour :term', - 'search_page_for_term' => 'Recherche de page pour :term', - 'search_chapter_for_term' => 'Recherche de chapitre pour :term', - 'search_book_for_term' => 'Recherche de livres pour :term', /** * Books diff --git a/resources/lang/nl/entities.php b/resources/lang/nl/entities.php index 610116c8b..d6975e130 100644 --- a/resources/lang/nl/entities.php +++ b/resources/lang/nl/entities.php @@ -43,18 +43,9 @@ return [ * Search */ 'search_results' => 'Zoekresultaten', - 'search_results_page' => 'Pagina Zoekresultaten', - 'search_results_chapter' => 'Hoofdstuk Zoekresultaten', - 'search_results_book' => 'Boek Zoekresultaten', 'search_clear' => 'Zoekopdracht wissen', - 'search_view_pages' => 'Bekijk alle gevonden pagina\'s', - 'search_view_chapters' => 'Bekijk alle gevonden hoofdstukken', - 'search_view_books' => 'Bekijk alle gevonden boeken', 'search_no_pages' => 'Er zijn geen pagina\'s gevonden', 'search_for_term' => 'Zoeken op :term', - 'search_page_for_term' => 'Pagina doorzoeken op :term', - 'search_chapter_for_term' => 'Hoofdstuk doorzoeken op :term', - 'search_book_for_term' => 'Boeken doorzoeken op :term', /** * Books diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php index 922342424..5a965fe62 100644 --- a/resources/lang/pt_BR/entities.php +++ b/resources/lang/pt_BR/entities.php @@ -43,18 +43,9 @@ return [ * Search */ 'search_results' => 'Resultado(s) da Pesquisa', - 'search_results_page' => 'Resultado(s) de Pesquisa de Página', - 'search_results_chapter' => 'Resultado(s) de Pesquisa de Capítulo', - 'search_results_book' => 'Resultado(s) de Pesquisa de Livro', 'search_clear' => 'Limpar Pesquisa', - 'search_view_pages' => 'Visualizar todas as páginas correspondentes', - 'search_view_chapters' => 'Visualizar todos os capítulos correspondentes', - 'search_view_books' => 'Visualizar todos os livros correspondentes', 'search_no_pages' => 'Nenhuma página corresponde à pesquisa', 'search_for_term' => 'Pesquisar por :term', - 'search_page_for_term' => 'Pesquisar Página por :term', - 'search_chapter_for_term' => 'Pesquisar Capítulo por :term', - 'search_book_for_term' => 'Pesquisar Livros por :term', /** * Books diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index d1f928912..1029b65fa 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -5,123 +5,203 @@
+ -
-

{{ trans('entities.search_results') }}

- - -
+

{{ trans('entities.search_results') }}

+
{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}
@include('partials/entity-list', ['entities' => $entities]) + @if ($hasNextPage) + {{ trans('entities.search_more') }} + @endif
-

Search Filters

+

{{ trans('entities.search_filters') }}

-
-

Content Type

+ +
{{ trans('entities.search_content_type') }}
- - - + + +
-

Exact Matches

+
{{ trans('entities.search_exact_matches') }}
+
- -
-

Tag Searches

+
{{ trans('entities.search_tags') }}
+
- -
-

Options

-
From dcde599709ba14637f9170f8a42fc83f9e39bce9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Apr 2017 19:16:07 +0100 Subject: [PATCH 11/14] Added chapter search Migrated book search to vue-based system. Updated old tag seached. Made chapter page layout widths same as book page. Closes #344 --- app/Http/Controllers/SearchController.php | 34 +++++---- app/Repos/EntityRepo.php | 2 +- app/Services/SearchService.php | 51 +++++++++++-- resources/assets/js/controllers.js | 33 -------- resources/assets/js/vues/entity-search.js | 44 +++++++++++ resources/assets/js/vues/vues.js | 3 +- resources/assets/sass/_lists.scss | 1 + resources/lang/en/entities.php | 1 + resources/views/books/show.blade.php | 27 ++++--- resources/views/chapters/show.blade.php | 75 ++++++++++++------- .../views/pages/sidebar-tree-list.blade.php | 6 +- routes/web.php | 1 + 12 files changed, 179 insertions(+), 99 deletions(-) create mode 100644 resources/assets/js/vues/entity-search.js diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index b65bca51e..bf8165afe 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -61,16 +61,24 @@ class SearchController extends Controller */ public function searchBook(Request $request, $bookId) { - if (!$request->has('term')) { - return redirect()->back(); - } - $searchTerm = $request->get('term'); - $searchWhereTerms = [['book_id', '=', $bookId]]; - $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms); - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms); - return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); + $term = $request->get('term', ''); + $results = $this->searchService->searchBook($bookId, $term); + return view('partials/entity-list', ['entities' => $results]); } + /** + * Searches all entities within a chapter. + * @param Request $request + * @param integer $chapterId + * @return \Illuminate\View\View + * @internal param string $searchTerm + */ + public function searchChapter(Request $request, $chapterId) + { + $term = $request->get('term', ''); + $results = $this->searchService->searchChapter($chapterId, $term); + return view('partials/entity-list', ['entities' => $results]); + } /** * Search for a list of entities and return a partial HTML response of matching entities. @@ -80,19 +88,13 @@ class SearchController extends Controller */ public function searchEntitiesAjax(Request $request) { - $entities = collect(); $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false; // Search for entities otherwise show most popular if ($searchTerm !== false) { - foreach (['page', 'chapter', 'book'] as $entityType) { - if ($entityTypes->contains($entityType)) { - // TODO - Update to new system - $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items()); - } - } - $entities = $entities->sortByDesc('title_relevance'); + $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}'; + $entities = $this->searchService->searchEntities($searchTerm)['results']; } else { $entityNames = $entityTypes->map(function ($type) { return 'BookStack\\' . ucfirst($type); diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index b1b69814d..975929639 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -569,7 +569,7 @@ class EntityRepo $draftPage->save(); $this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); - + $this->searchService->indexEntity($draftPage); return $draftPage; } diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 7ecfb95c7..ec4889e50 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -8,6 +8,7 @@ use BookStack\SearchTerm; use Illuminate\Database\Connection; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Collection; class SearchService { @@ -86,6 +87,35 @@ class SearchService ]; } + + /** + * Search a book for entities + * @param integer $bookId + * @param string $searchString + * @return Collection + */ + public function searchBook($bookId, $searchString) + { + $terms = $this->parseSearchString($searchString); + $results = collect(); + $pages = $this->buildEntitySearchQuery($terms, 'page')->where('book_id', '=', $bookId)->take(20)->get(); + $chapters = $this->buildEntitySearchQuery($terms, 'chapter')->where('book_id', '=', $bookId)->take(20)->get(); + return $results->merge($pages)->merge($chapters)->sortByDesc('score')->take(20); + } + + /** + * Search a book for entities + * @param integer $chapterId + * @param string $searchString + * @return Collection + */ + public function searchChapter($chapterId, $searchString) + { + $terms = $this->parseSearchString($searchString); + $pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get(); + return $pages->sortByDesc('score'); + } + /** * Search across a particular entity type. * @param array $terms @@ -96,6 +126,21 @@ class SearchService * @return \Illuminate\Database\Eloquent\Collection|int|static[] */ public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false) + { + $query = $this->buildEntitySearchQuery($terms, $entityType); + if ($getCount) return $query->count(); + + $query = $query->skip(($page-1) * $count)->take($count); + return $query->get(); + } + + /** + * Create a search query for an entity + * @param array $terms + * @param string $entityType + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function buildEntitySearchQuery($terms, $entityType = 'page') { $entity = $this->getEntity($entityType); $entitySelect = $entity->newQuery(); @@ -137,11 +182,7 @@ class SearchService if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue); } - $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); - if ($getCount) return $query->count(); - - $query = $query->skip(($page-1) * $count)->take($count); - return $query->get(); + return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); } diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index c5baecf16..6a88aa811 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -259,39 +259,6 @@ module.exports = function (ngApp, events) { }]); - - ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) { - $scope.searching = false; - $scope.searchTerm = ''; - $scope.searchResults = ''; - - $scope.searchBook = function (e) { - e.preventDefault(); - let term = $scope.searchTerm; - if (term.length == 0) return; - $scope.searching = true; - $scope.searchResults = ''; - let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId); - searchUrl += '?term=' + encodeURIComponent(term); - $http.get(searchUrl).then((response) => { - $scope.searchResults = $sce.trustAsHtml(response.data); - }); - }; - - $scope.checkSearchForm = function () { - if ($scope.searchTerm.length < 1) { - $scope.searching = false; - } - }; - - $scope.clearSearch = function () { - $scope.searching = false; - $scope.searchTerm = ''; - }; - - }]); - - ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', function ($scope, $http, $attrs, $interval, $timeout, $sce) { diff --git a/resources/assets/js/vues/entity-search.js b/resources/assets/js/vues/entity-search.js new file mode 100644 index 000000000..7266bf33d --- /dev/null +++ b/resources/assets/js/vues/entity-search.js @@ -0,0 +1,44 @@ +let data = { + id: null, + type: '', + searching: false, + searchTerm: '', + searchResults: '', +}; + +let computed = { + +}; + +let methods = { + + searchBook() { + if (this.searchTerm.trim().length === 0) return; + this.searching = true; + this.searchResults = ''; + let url = window.baseUrl(`/search/${this.type}/${this.id}`); + url += `?term=${encodeURIComponent(this.searchTerm)}`; + this.$http.get(url).then(resp => { + this.searchResults = resp.data; + }); + }, + + checkSearchForm() { + this.searching = this.searchTerm > 0; + }, + + clearSearch() { + this.searching = false; + this.searchTerm = ''; + } + +}; + +function mounted() { + this.id = Number(this.$el.getAttribute('entity-id')); + this.type = this.$el.getAttribute('entity-type'); +} + +module.exports = { + data, computed, methods, mounted +}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index 832a5415d..8cc1dd656 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -5,7 +5,8 @@ function exists(id) { } let vueMapping = { - 'search-system': require('./search') + 'search-system': require('./search'), + 'entity-dashboard': require('./entity-search'), }; Object.keys(vueMapping).forEach(id => { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 6acc47468..051268926 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -109,6 +109,7 @@ transition-property: right, border; border-left: 0px solid #FFF; background-color: #FFF; + max-width: 320px; &.fixed { background-color: #FFF; z-index: 5; diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 66c2e8042..8644f7a4a 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -120,6 +120,7 @@ return [ 'chapters_empty' => 'No pages are currently in this chapter.', 'chapters_permissions_active' => 'Chapter Permissions Active', 'chapters_permissions_success' => 'Chapter Permissions Updated', + 'chapters_search_this' => 'Search this chapter', /** * Pages diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index f5e08b2f6..adfec4525 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -50,15 +50,15 @@
-
+

{{$book->name}}

-
-

{{$book->description}}

+
+

{{$book->description}}

-
+

@if(count($bookChildren) > 0) @foreach($bookChildren as $childElement) @@ -81,12 +81,12 @@ @include('partials.entity-meta', ['entity' => $book])
-
-

{{ trans('entities.search_results') }} {{ trans('entities.search_clear') }}

-
+
+

{{ trans('entities.search_results') }} {{ trans('entities.search_clear') }}

+
@include('partials/loading-icon')
-
+
@@ -94,6 +94,7 @@
+ @if($book->restricted)

@if(userCan('restrictions-manage', $book)) @@ -103,14 +104,16 @@ @endif

@endif + -
+ +

{{ trans('entities.recent_activity') }}

@include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index 28c34eef2..d4126cbcc 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -47,40 +47,50 @@
-
+
-
+

{{ $chapter->name }}

-

{{ $chapter->description }}

+
+

{{ $chapter->description }}

- @if(count($pages) > 0) -
-
- @foreach($pages as $page) - @include('pages/list-item', ['page' => $page]) + @if(count($pages) > 0) +

- @endforeach -
- @else -
-

{{ trans('entities.chapters_empty') }}

-

- @if(userCan('page-create', $chapter)) - {{ trans('entities.books_empty_create_page') }} - @endif - @if(userCan('page-create', $chapter) && userCan('book-update', $book)) -   -{{ trans('entities.books_empty_or') }}-    - @endif - @if(userCan('book-update', $book)) - {{ trans('entities.books_empty_sort_current_book') }} - @endif -

-
- @endif + @foreach($pages as $page) + @include('pages/list-item', ['page' => $page]) +
+ @endforeach +
+ @else +
+

{{ trans('entities.chapters_empty') }}

+

+ @if(userCan('page-create', $chapter)) + {{ trans('entities.books_empty_create_page') }} + @endif + @if(userCan('page-create', $chapter) && userCan('book-update', $book)) +   -{{ trans('entities.books_empty_or') }}-    + @endif + @if(userCan('book-update', $book)) + {{ trans('entities.books_empty_sort_current_book') }} + @endif +

+
+ @endif - @include('partials.entity-meta', ['entity' => $chapter]) + @include('partials.entity-meta', ['entity' => $chapter]) +
+ +
+

{{ trans('entities.search_results') }} {{ trans('entities.search_clear') }}

+
+ @include('partials/loading-icon') +
+
+
-
+
@if($book->restricted || $chapter->restricted)
@@ -105,7 +115,16 @@
@endif + + @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) +
diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index f366e9e9b..faae6420a 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -3,13 +3,13 @@ @if(isset($page) && $page->tags->count() > 0)
-
Page Tags
+
{{ trans('entities.page_tags') }}
@foreach($page->tags as $tag) - - @if($tag->value) @endif + + @if($tag->value) @endif @endforeach diff --git a/routes/web.php b/routes/web.php index dad7a55e5..8ecfd9465 100644 --- a/routes/web.php +++ b/routes/web.php @@ -125,6 +125,7 @@ Route::group(['middleware' => 'auth'], function () { // Search Route::get('/search', 'SearchController@search'); Route::get('/search/book/{bookId}', 'SearchController@searchBook'); + Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter'); // Other Pages Route::get('/', 'HomeController@index'); From 73844b9eeb7d193236dc44fa8f86991ffed8301d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Apr 2017 19:31:11 +0100 Subject: [PATCH 12/14] Enabled type search filter in book search --- app/Services/SearchService.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index ec4889e50..a3186e8f4 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -97,10 +97,16 @@ class SearchService public function searchBook($bookId, $searchString) { $terms = $this->parseSearchString($searchString); + $entityTypes = ['page', 'chapter']; + $entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes; + $results = collect(); - $pages = $this->buildEntitySearchQuery($terms, 'page')->where('book_id', '=', $bookId)->take(20)->get(); - $chapters = $this->buildEntitySearchQuery($terms, 'chapter')->where('book_id', '=', $bookId)->take(20)->get(); - return $results->merge($pages)->merge($chapters)->sortByDesc('score')->take(20); + foreach ($entityTypesToSearch as $entityType) { + if (!in_array($entityType, $entityTypes)) continue; + $search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get(); + $results = $results->merge($search); + } + return $results->sortByDesc('score')->take(20); } /** From a15b179676b849d823fd8614845589fc4f6cc046 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 16 Apr 2017 10:47:44 +0100 Subject: [PATCH 13/14] Updated testcases for new search system. Finishes implementation of new search system. Closes #271 Closes #344 Fixes #285 Fixes #269 Closes #64 --- app/Repos/EntityRepo.php | 7 +- tests/Entity/EntitySearchTest.php | 197 ++++++++++++++++++------------ tests/TestCase.php | 12 ++ 3 files changed, 135 insertions(+), 81 deletions(-) diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 975929639..62b8cf324 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -219,6 +219,7 @@ class EntityRepo * @param int $count * @param int $page * @param bool|callable $additionalQuery + * @return Collection */ public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false) { @@ -237,6 +238,7 @@ class EntityRepo * @param int $count * @param int $page * @param bool|callable $additionalQuery + * @return Collection */ public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false) { @@ -330,7 +332,7 @@ class EntityRepo if ($rawEntity->entity_type === 'BookStack\\Page') { $entities[$index] = $this->page->newFromBuilder($rawEntity); if ($renderPages) { - $entities[$index]->html = $rawEntity->description; + $entities[$index]->html = $rawEntity->html; $entities[$index]->html = $this->renderPage($entities[$index]); }; } else if ($rawEntity->entity_type === 'BookStack\\Chapter') { @@ -357,6 +359,7 @@ class EntityRepo * Get the child items for a chapter sorted by priority but * with draft items floated to the top. * @param Chapter $chapter + * @return \Illuminate\Database\Eloquent\Collection|static[] */ public function getChapterChildren(Chapter $chapter) { @@ -470,7 +473,7 @@ class EntityRepo /** * Update entity details from request input. - * Use for books and chapters + * Used for books and chapters * @param string $type * @param Entity $entityModel * @param array $input diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 4ef8d46fb..9f77972c4 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -1,6 +1,7 @@ first(); $page = $book->pages->first(); - $this->asAdmin() - ->visit('/') - ->type($page->name, 'term') - ->press('header-search-box-button') - ->see('Search Results') - ->seeInElement('.entity-list', $page->name) - ->clickInElement('.entity-list', $page->name) - ->seePageIs($page->getUrl()); + $search = $this->asEditor()->get('/search?term=' . urlencode($page->name)); + $search->assertSee('Search Results'); + $search->assertSee($page->name); } public function test_invalid_page_search() { - $this->asAdmin() - ->visit('/') - ->type('

test

', 'term') - ->press('header-search-box-button') - ->see('Search Results') - ->seeStatusCode(200); + $resp = $this->asEditor()->get('/search?term=' . urlencode('

test

')); + $resp->assertSee('Search Results'); + $resp->assertStatus(200); + $this->get('/search?term=cat+-')->assertStatus(200); } - public function test_empty_search_redirects_back() + public function test_empty_search_shows_search_page() { - $this->asAdmin() - ->visit('/') - ->visit('/search/all') - ->seePageIs('/'); + $res = $this->asEditor()->get('/search'); + $res->assertStatus(200); + } + + public function test_searching_accents_and_small_terms() + { + $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content {a2 orange dog']); + $this->asEditor(); + + $accentSearch = $this->get('/search?term=' . urlencode('áéíí')); + $accentSearch->assertStatus(200)->assertSee($page->name); + + $smallSearch = $this->get('/search?term=' . urlencode('{a')); + $smallSearch->assertStatus(200)->assertSee($page->name); } public function test_book_search() @@ -42,57 +46,20 @@ class EntitySearchTest extends BrowserKitTest $page = $book->pages->last(); $chapter = $book->chapters->last(); - $this->asAdmin() - ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name)) - ->see($page->name) + $pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name)); + $pageTestResp->assertSee($page->name); - ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name)) - ->see($chapter->name); + $chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name)); + $chapterTestResp->assertSee($chapter->name); } - public function test_empty_book_search_redirects_back() + public function test_chapter_search() { - $book = \BookStack\Book::all()->first(); - $this->asAdmin() - ->visit('/books') - ->visit('/search/book/' . $book->id . '?term=') - ->seePageIs('/books'); - } + $chapter = \BookStack\Chapter::has('pages')->first(); + $page = $chapter->pages[0]; - - public function test_pages_search_listing() - { - $page = \BookStack\Page::all()->last(); - $this->asAdmin()->visit('/search/pages?term=' . $page->name) - ->see('Page Search Results')->see('.entity-list', $page->name); - } - - public function test_chapters_search_listing() - { - $chapter = \BookStack\Chapter::all()->last(); - $this->asAdmin()->visit('/search/chapters?term=' . $chapter->name) - ->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name); - } - - public function test_search_quote_term_preparation() - { - $termString = '"192" cat "dog hat"'; - $repo = $this->app[\BookStack\Repos\EntityRepo::class]; - $preparedTerms = $repo->prepareSearchTerms($termString); - $this->assertTrue($preparedTerms === ['"192"','"dog hat"', 'cat']); - } - - public function test_books_search_listing() - { - $book = \BookStack\Book::all()->last(); - $this->asAdmin()->visit('/search/books?term=' . $book->name) - ->see('Book Search Results')->see('.entity-list', $book->name); - } - - public function test_searching_hypen_doesnt_break() - { - $this->visit('/search/all?term=cat+-') - ->seeStatusCode(200); + $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name)); + $pageTestResp->assertSee($page->name); } public function test_tag_search() @@ -114,27 +81,99 @@ class EntitySearchTest extends BrowserKitTest $pageB = \BookStack\Page::all()->last(); $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']); - $this->asAdmin()->visit('/search/all?term=%5Banimal%5D') - ->seeLink($pageA->name) - ->seeLink($pageB->name); + $this->asEditor(); + $tNameSearch = $this->get('/search?term=%5Banimal%5D'); + $tNameSearch->assertSee($pageA->name)->assertSee($pageB->name); - $this->visit('/search/all?term=%5Bcolor%5D') - ->seeLink($pageA->name) - ->dontSeeLink($pageB->name); + $tNameSearch2 = $this->get('/search?term=%5Bcolor%5D'); + $tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name); - $this->visit('/search/all?term=%5Banimal%3Dcat%5D') - ->seeLink($pageA->name) - ->dontSeeLink($pageB->name); + $tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D'); + $tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name); + } + public function test_exact_searches() + { + $page = $this->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']); + + $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"')); + $exactSearchA->assertStatus(200)->assertSee($page->name); + + $exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('"story not about an orange"')); + $exactSearchB->assertStatus(200)->assertDontSee($page->name); + } + + public function test_search_filters() + { + $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']); + $this->asEditor(); + $editorId = $this->getEditor()->id; + + // Viewed filter searches + $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name); + $this->get($page->getUrl()); + $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name); + + // User filters + $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorId.'}'))->assertDontSee($page->name); + $page->created_by = $editorId; + $page->save(); + $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {created_by:'.$editorId.'}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name); + $page->updated_by = $editorId; + $page->save(); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorId.'}'))->assertSee($page->name); + + // Content filters + $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name); + + // Restricted filter + $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name); + $page->restricted = true; + $page->save(); + $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name); + + // Date filters + $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name); + $page->updated_at = '2037-02-01'; + $page->save(); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name); + + $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name); + $page->created_at = '2037-02-01'; + $page->save(); + $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name); } public function test_ajax_entity_search() { $page = \BookStack\Page::all()->last(); $notVisitedPage = \BookStack\Page::first(); - $this->visit($page->getUrl()); - $this->asAdmin()->visit('/ajax/search/entities?term=' . $page->name)->see('.entity-list', $page->name); - $this->asAdmin()->visit('/ajax/search/entities?types=book&term=' . $page->name)->dontSee('.entity-list', $page->name); - $this->asAdmin()->visit('/ajax/search/entities')->see('.entity-list', $page->name)->dontSee($notVisitedPage->name); + + // Visit the page to make popular + $this->asEditor()->get($page->getUrl()); + + $normalSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name)); + $normalSearch->assertSee($page->name); + + $bookSearch = $this->get('/ajax/search/entities?types=book&term=' . urlencode($page->name)); + $bookSearch->assertDontSee($page->name); + + $defaultListTest = $this->get('/ajax/search/entities'); + $defaultListTest->assertSee($page->name); + $defaultListTest->assertDontSee($notVisitedPage->name); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index f3f36ca1c..b008080d9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) { return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book); } + + /** + * Create and return a new test page + * @param array $input + * @return Chapter + */ + public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) { + $book = Book::first(); + $entityRepo = $this->app[EntityRepo::class]; + $draftPage = $entityRepo->getDraftPage($book); + return $entityRepo->publishPageDraft($draftPage, $input); + } } \ No newline at end of file From 8ed9f75d57699da58a411522fe891cca326b7f35 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 16 Apr 2017 10:54:23 +0100 Subject: [PATCH 14/14] Fixed model extending mis-use --- app/SearchTerm.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/SearchTerm.php b/app/SearchTerm.php index a7e3814f8..50df34021 100644 --- a/app/SearchTerm.php +++ b/app/SearchTerm.php @@ -1,7 +1,5 @@
value) colspan="2" @endif>{{ $tag->name }}{{$tag->value}}value) colspan="2" @endif>{{ $tag->name }}{{$tag->value}}