From 070d4aeb6ca9af987c551aede0e3b37688a0808e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Mar 2017 12:48:44 +0000 Subject: [PATCH 01/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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 9b639f715f7ad9b88893cfcde58e62e9af3f004d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vlado=20Jendro=C4=BE?= Date: Tue, 11 Apr 2017 23:20:52 +0200 Subject: [PATCH 09/19] Add Slovak translation --- resources/lang/en/settings.php | 1 + resources/lang/sk/activities.php | 40 ++++++ resources/lang/sk/auth.php | 76 +++++++++++ resources/lang/sk/common.php | 58 ++++++++ resources/lang/sk/components.php | 24 ++++ resources/lang/sk/entities.php | 226 +++++++++++++++++++++++++++++++ resources/lang/sk/errors.php | 70 ++++++++++ resources/lang/sk/pagination.php | 19 +++ resources/lang/sk/passwords.php | 22 +++ resources/lang/sk/settings.php | 111 +++++++++++++++ resources/lang/sk/validation.php | 108 +++++++++++++++ 11 files changed, 755 insertions(+) create mode 100644 resources/lang/sk/activities.php create mode 100644 resources/lang/sk/auth.php create mode 100644 resources/lang/sk/common.php create mode 100644 resources/lang/sk/components.php create mode 100644 resources/lang/sk/entities.php create mode 100644 resources/lang/sk/errors.php create mode 100644 resources/lang/sk/pagination.php create mode 100644 resources/lang/sk/passwords.php create mode 100644 resources/lang/sk/settings.php create mode 100644 resources/lang/sk/validation.php diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index fa60f99a8..31163e87e 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -120,6 +120,7 @@ return [ 'fr' => 'Français', 'nl' => 'Nederlands', 'pt_BR' => 'Português do Brasil', + 'sk' => 'Slovensky', ] /////////////////////////////////// ]; diff --git a/resources/lang/sk/activities.php b/resources/lang/sk/activities.php new file mode 100644 index 000000000..1d87d3fa3 --- /dev/null +++ b/resources/lang/sk/activities.php @@ -0,0 +1,40 @@ + 'vytvoril stránku', + 'page_create_notification' => 'Stránka úspešne vytvorená', + 'page_update' => 'aktualizoval stránku', + 'page_update_notification' => 'Stránka úspešne aktualizovaná', + 'page_delete' => 'odstránil stránku', + 'page_delete_notification' => 'Stránka úspešne odstránená', + 'page_restore' => 'obnovil stránku', + 'page_restore_notification' => 'Stránka úspešne obnovená', + 'page_move' => 'presunul stránku', + + // Chapters + 'chapter_create' => 'vytvoril kapitolu', + 'chapter_create_notification' => 'Kapitola úspešne vytvorená', + 'chapter_update' => 'aktualizoval kapitolu', + 'chapter_update_notification' => 'Kapitola úspešne aktualizovaná', + 'chapter_delete' => 'odstránil kapitolu', + 'chapter_delete_notification' => 'Kapitola úspešne odstránená', + 'chapter_move' => 'presunul kapitolu', + + // Books + 'book_create' => 'vytvoril knihu', + 'book_create_notification' => 'Kniha úspešne vytvorená', + 'book_update' => 'aktualizoval knihu', + 'book_update_notification' => 'Kniha úspešne aktualizovaná', + 'book_delete' => 'odstránil knihu', + 'book_delete_notification' => 'Kniha úspešne odstránená', + 'book_sort' => 'zoradil knihu', + 'book_sort_notification' => 'Kniha úspešne znovu zoradená', + +]; diff --git a/resources/lang/sk/auth.php b/resources/lang/sk/auth.php new file mode 100644 index 000000000..2fa69ac3e --- /dev/null +++ b/resources/lang/sk/auth.php @@ -0,0 +1,76 @@ + 'Tieto údaje nesedia s našimi záznamami.', + 'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.', + + /** + * Login & Register + */ + 'sign_up' => 'Registrácia', + 'log_in' => 'Prihlásenie', + 'log_in_with' => 'Prihlásiť sa cez :socialDriver', + 'sign_up_with' => 'Registrovať sa cez :socialDriver', + 'logout' => 'Odhlásenie', + + 'name' => 'Meno', + 'username' => 'Používateľské meno', + 'email' => 'Email', + 'password' => 'Heslo', + 'password_confirm' => 'Potvrdiť heslo', + 'password_hint' => 'Musí mať viac ako 5 znakov', + 'forgot_password' => 'Zabudli ste heslo?', + 'remember_me' => 'Zapamätať si ma', + 'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.', + 'create_account' => 'Vytvoriť účet', + 'social_login' => 'Sociálne prihlásenie', + 'social_registration' => 'Sociálna registrácia', + 'social_registration_text' => 'Registrovať sa a prihlásiť sa použitím inej služby.', + + 'register_thanks' => 'Ďakujeme zaregistráciu!', + 'register_confirm' => 'Skontrolujte prosím svoj email a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.', + 'registrations_disabled' => 'Registrácie sú momentálne zablokované', + 'registration_email_domain_invalid' => 'Táto emailová doména nemá prístup k tejto aplikácii', + 'register_success' => 'Ďakujeme za registráciu! Teraz ste registrovaný a prihlásený.', + + + /** + * Password Reset + */ + 'reset_password' => 'Reset hesla', + 'reset_password_send_instructions' => 'Zadajte svoj email nižšie a bude Vám odoslaný email s odkazom pre reset hesla.', + 'reset_password_send_button' => 'Poslať odkaz na reset hesla', + 'reset_password_sent_success' => 'Odkaz na reset hesla bol poslaný na :email.', + 'reset_password_success' => 'Vaše heslo bolo úspešne resetované.', + + 'email_reset_subject' => 'Reset Vášho :appName hesla', + 'email_reset_text' => 'Tento email Ste dostali pretože sme dostali požiadavku na reset hesla pre Váš účet.', + 'email_reset_not_requested' => 'Ak ste nepožiadali o reset hesla, nemusíte nič robiť.', + + + /** + * Email Confirmation + */ + 'email_confirm_subject' => 'Potvrdiť email na :appName', + 'email_confirm_greeting' => 'Ďakujeme za pridanie sa k :appName!', + 'email_confirm_text' => 'Prosím potvrďte Vašu emailovú adresu kliknutím na tlačidlo nižšie:', + 'email_confirm_action' => 'Potvrdiť email', + 'email_confirm_send_error' => 'Je požadované overenie emailu, ale systém nemohol odoslať email. Kontaktujte administrátora by ste sa uistili, že email je nastavený správne.', + 'email_confirm_success' => 'Váš email bol overený!', + 'email_confirm_resent' => 'Potvrdzujúci email bol poslaný znovu, skontrolujte prosím svoju emailovú schránku.', + + 'email_not_confirmed' => 'Emailová adresa nebola overená', + 'email_not_confirmed_text' => 'Vaša emailová adresa nebola zatiaľ overená.', + 'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v emaili, ktorý bol poslaný krátko po Vašej registrácii.', + 'email_not_confirmed_resend' => 'Ak nemôžete násť email, môžete znova odoslať overovací email odoslaním doleuvedeného formulára.', + 'email_not_confirmed_resend_button' => 'Znova odoslať overovací email', +]; diff --git a/resources/lang/sk/common.php b/resources/lang/sk/common.php new file mode 100644 index 000000000..100981597 --- /dev/null +++ b/resources/lang/sk/common.php @@ -0,0 +1,58 @@ + 'Zrušiť', + 'confirm' => 'Potvrdiť', + 'back' => 'Späť', + 'save' => 'Uložiť', + 'continue' => 'Pokračovať', + 'select' => 'Vybrať', + + /** + * Form Labels + */ + 'name' => 'Meno', + 'description' => 'Popis', + 'role' => 'Rola', + + /** + * Actions + */ + 'actions' => 'Akcie', + 'view' => 'Zobraziť', + 'create' => 'Vytvoriť', + 'update' => 'Aktualizovať', + 'edit' => 'Editovať', + 'sort' => 'Zoradiť', + 'move' => 'Presunúť', + 'delete' => 'Zmazať', + 'search' => 'Hľadť', + 'search_clear' => 'Vyčistiť hľadanie', + 'reset' => 'Reset', + 'remove' => 'Odstrániť', + + + /** + * Misc + */ + 'deleted_user' => 'Odstránený používateľ', + 'no_activity' => 'Žiadna aktivita na zobrazenie', + 'no_items' => 'Žiadne položky nie sú dostupné', + 'back_to_top' => 'Späť nahor', + 'toggle_details' => 'Prepnúť detaily', + + /** + * Header + */ + 'view_profile' => 'Zobraziť profil', + 'edit_profile' => 'Upraviť profil', + + /** + * Email Content + */ + 'email_action_help' => 'Ak máte problém klinkúť na tlačidlo ":actionText", skopírujte a vložte URL uvedenú nižšie do Vášho prehliadača:', + 'email_rights' => 'Všetky práva vyhradené', +]; diff --git a/resources/lang/sk/components.php b/resources/lang/sk/components.php new file mode 100644 index 000000000..f4fa92043 --- /dev/null +++ b/resources/lang/sk/components.php @@ -0,0 +1,24 @@ + 'Vybrať obrázok', + 'image_all' => 'Všetko', + 'image_all_title' => 'Zobraziť všetky obrázky', + 'image_book_title' => 'Zobraziť obrázky nahrané do tejto knihy', + 'image_page_title' => 'Zobraziť obrázky nahrané do tejto stránky', + 'image_search_hint' => 'Hľadať obrázok podľa názvu', + 'image_uploaded' => 'Nahrané :uploadedDate', + 'image_load_more' => 'Načítať viac', + 'image_image_name' => 'Názov obrázka', + 'image_delete_confirm' => 'Tento obrázok je použitý na stránkach uvedených nižšie, kliknite znova na zmazať pre potvrdenie zmazania tohto obrázka.', + 'image_select_image' => 'Vybrať obrázok', + 'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie', + 'images_deleted' => 'Obrázky zmazané', + 'image_preview' => 'Náhľad obrázka', + 'image_upload_success' => 'Obrázok úspešne nahraný', + 'image_update_success' => 'Detaily obrázka úspešne aktualizované', + 'image_delete_success' => 'Obrázok úspešne zmazaný' +]; diff --git a/resources/lang/sk/entities.php b/resources/lang/sk/entities.php new file mode 100644 index 000000000..e70864753 --- /dev/null +++ b/resources/lang/sk/entities.php @@ -0,0 +1,226 @@ + 'Nedávno vytvorené', + 'recently_created_pages' => 'Nedávno vytvorené stránky', + 'recently_updated_pages' => 'Nedávno aktualizované stránky', + 'recently_created_chapters' => 'Nedávno vytvorené kapitoly', + 'recently_created_books' => 'Nedávno vytvorené knihy', + 'recently_update' => 'Nedávno aktualizované', + 'recently_viewed' => 'Nedávno zobrazené', + 'recent_activity' => 'Nedávna aktivita', + 'create_now' => 'Vytvoriť teraz', + 'revisions' => 'Revízie', + 'meta_created' => 'Vytvorené :timeLength', + 'meta_created_name' => 'Vytvorené :timeLength používateľom :user', + 'meta_updated' => 'Aktualizované :timeLength', + 'meta_updated_name' => 'Aktualizované :timeLength používateľom :user', + 'x_pages' => ':count stránok', + 'entity_select' => 'Entita vybraná', + 'images' => 'Obrázky', + 'my_recent_drafts' => 'Moje nedávne koncepty', + 'my_recently_viewed' => 'Nedávno mnou zobrazené', + 'no_pages_viewed' => 'Nepozreli ste si žiadne stránky', + 'no_pages_recently_created' => 'Žiadne stránky neboli nedávno vytvorené', + 'no_pages_recently_updated' => 'Žiadne stránky neboli nedávno aktualizované', + 'export' => 'Export', + 'export_html' => 'Contained Web File', + 'export_pdf' => 'PDF súbor', + 'export_text' => 'Súbor s čistým textom', + + /** + * Permissions and restrictions + */ + 'permissions' => 'Oprávnenia', + 'permissions_intro' => 'Ak budú tieto oprávnenia povolené, budú mať prioritu pred oprávneniami roly.', + 'permissions_enable' => 'Povoliť vlastné oprávnenia', + 'permissions_save' => 'Uložiť oprávnenia', + + /** + * Search + */ + 'search_results' => 'Výsledky hľadania', + 'search_results_page' => 'Výsledky hľadania stránky', + 'search_results_chapter' => 'Výsledky hľadania kapitoly', + 'search_results_book' => 'Výsledky hľadania knihy', + 'search_clear' => 'Vyčistiť hľadanie', + 'search_view_pages' => 'Zobraziť všetky vyhovujúce stránky', + 'search_view_chapters' => 'Zobraziť všetky vyhovujúce kapitoly', + 'search_view_books' => 'Zobraziť všetky vyhovujúce knihy', + 'search_no_pages' => 'Žiadne stránky nevyhovujú tomuto hľadaniu', + 'search_for_term' => 'Hľadať :term', + 'search_page_for_term' => 'Hľadať :term medzi stránkami', + 'search_chapter_for_term' => 'Hľadať :term medzi kapitolami', + 'search_book_for_term' => 'Hľadať :term medzi knihami', + + /** + * Books + */ + 'book' => 'Kniha', + 'books' => 'Knihy', + 'books_empty' => 'Žiadne knihy neboli vytvorené', + 'books_popular' => 'Populárne knihy', + 'books_recent' => 'Nedávne knihy', + 'books_popular_empty' => 'Najpopulárnejšie knihy sa objavia tu.', + 'books_create' => 'Vytvoriť novú knihu', + 'books_delete' => 'Zmazať knihu', + 'books_delete_named' => 'Zmazať knihu :bookName', + 'books_delete_explain' => 'Toto zmaže knihu s názvom \':bookName\', všetky stránky a kapitoly budú odstránené.', + 'books_delete_confirmation' => 'Ste si istý, že chcete zmazať túto knihu?', + 'books_edit' => 'Upraviť knihu', + 'books_edit_named' => 'Upraviť knihu :bookName', + 'books_form_book_name' => 'Názov knihy', + 'books_save' => 'Uložiť knihu', + 'books_permissions' => 'Oprávnenia knihy', + 'books_permissions_updated' => 'Oprávnenia knihy aktualizované', + 'books_empty_contents' => 'Pre túto knihu neboli vytvorené žiadne stránky alebo kapitoly.', + 'books_empty_create_page' => 'Vytvoriť novú stránku', + 'books_empty_or' => 'alebo', + 'books_empty_sort_current_book' => 'Zoradiť aktuálnu knihu', + 'books_empty_add_chapter' => 'Pridať kapitolu', + 'books_permissions_active' => 'Oprávnenia knihy aktívne', + 'books_search_this' => 'Hľadať v tejto knihe', + 'books_navigation' => 'Navigácia knihy', + 'books_sort' => 'Zoradiť obsah knihy', + 'books_sort_named' => 'Zoradiť knihu :bookName', + 'books_sort_show_other' => 'Zobraziť ostatné knihy', + 'books_sort_save' => 'Uložiť nové zoradenie', + + /** + * Chapters + */ + 'chapter' => 'Kapitola', + 'chapters' => 'Kapitoly', + 'chapters_popular' => 'Populárne kapitoly', + 'chapters_new' => 'Nová kapitola', + 'chapters_create' => 'Vytvoriť novú kapitolu', + 'chapters_delete' => 'Zmazať kapitolu', + 'chapters_delete_named' => 'Zmazať kapitolu :chapterName', + 'chapters_delete_explain' => 'Toto zmaže kapitolu menom \':chapterName\', všetky stránky budú ostránené + a pridané priamo do rodičovskej knihy.', + 'chapters_delete_confirm' => 'Ste si istý, že chcete zmazať túto kapitolu?', + 'chapters_edit' => 'Upraviť kapitolu', + 'chapters_edit_named' => 'Upraviť kapitolu :chapterName', + 'chapters_save' => 'Uložiť kapitolu', + 'chapters_move' => 'Presunúť kapitolu', + 'chapters_move_named' => 'Presunúť kapitolu :chapterName', + 'chapter_move_success' => 'Kapitola presunutá do :bookName', + 'chapters_permissions' => 'Oprávnenia kapitoly', + 'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.', + 'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne', + 'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované', + + /** + * Pages + */ + 'page' => 'Stránka', + 'pages' => 'Stránky', + 'pages_popular' => 'Populárne stránky', + 'pages_new' => 'Nová stránka', + 'pages_attachments' => 'Prílohy', + 'pages_navigation' => 'Navigácia', + 'pages_delete' => 'Zmazať stránku', + 'pages_delete_named' => 'Zmazať stránku :pageName', + 'pages_delete_draft_named' => 'Zmazať koncept :pageName', + 'pages_delete_draft' => 'Zmazať koncept', + 'pages_delete_success' => 'Stránka zmazaná', + 'pages_delete_draft_success' => 'Koncept stránky zmazaný', + 'pages_delete_confirm' => 'Ste si istý, že chcete zmazať túto stránku?', + 'pages_delete_draft_confirm' => 'Ste si istý, že chcete zmazať tento koncept stránky?', + 'pages_editing_named' => 'Upraviť stránku :pageName', + 'pages_edit_toggle_header' => 'Prepnúť hlavičku', + 'pages_edit_save_draft' => 'Uložiť koncept', + 'pages_edit_draft' => 'Upraviť koncept stránky', + 'pages_editing_draft' => 'Upravuje sa koncept', + 'pages_editing_page' => 'Upravuje sa stránka', + 'pages_edit_draft_save_at' => 'Koncept uložený pod ', + 'pages_edit_delete_draft' => 'Uložiť koncept', + 'pages_edit_discard_draft' => 'Zrušiť koncept', + 'pages_edit_set_changelog' => 'Nastaviť záznam zmien', + 'pages_edit_enter_changelog_desc' => 'Zadajte krátky popis zmien, ktoré ste urobili', + 'pages_edit_enter_changelog' => 'Zadať záznam zmien', + 'pages_save' => 'Uložiť stránku', + 'pages_title' => 'Titulok stránky', + 'pages_name' => 'Názov stránky', + 'pages_md_editor' => 'Editor', + 'pages_md_preview' => 'Náhľad', + 'pages_md_insert_image' => 'Vložiť obrázok', + 'pages_md_insert_link' => 'Vložiť odkaz na entitu', + 'pages_not_in_chapter' => 'Stránka nie je v kapitole', + 'pages_move' => 'Presunúť stránku', + 'pages_move_success' => 'Stránka presunutá do ":parentName"', + 'pages_permissions' => 'Oprávnenia stránky', + 'pages_permissions_success' => 'Oprávnenia stránky aktualizované', + 'pages_revisions' => 'Revízie stránky', + 'pages_revisions_named' => 'Revízie stránky :pageName', + 'pages_revision_named' => 'Revízia stránky :pageName', + 'pages_revisions_created_by' => 'Vytvoril', + 'pages_revisions_date' => 'Dátum revízie', + 'pages_revisions_changelog' => 'Záznam zmien', + 'pages_revisions_changes' => 'Zmeny', + 'pages_revisions_current' => 'Aktuálna verzia', + 'pages_revisions_preview' => 'Náhľad', + 'pages_revisions_restore' => 'Obnoviť', + 'pages_revisions_none' => 'Táto stránka nemá žiadne revízie', + 'pages_copy_link' => 'Kopírovať odkaz', + 'pages_permissions_active' => 'Oprávnienia stránky aktívne', + 'pages_initial_revision' => 'Prvé zverejnenie', + 'pages_initial_name' => 'Nová stránka', + 'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.', + 'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.', + 'pages_draft_edit_active' => [ + 'start_a' => ':count používateľov začalo upravovať túto stránku', + 'start_b' => ':userName začal upravovať túto stránku', + 'time_a' => 'odkedy boli stránky naposledy aktualizované', + 'time_b' => 'za posledných :minCount minút', + 'message' => ':start :time. Dávajte pozor aby ste si navzájom neprepísali zmeny!', + ], + 'pages_draft_discarded' => 'Koncept ostránený, aktuálny obsah stránky bol nahraný do editora', + + /** + * Editor sidebar + */ + 'page_tags' => 'Štítky stránok', + 'tag' => 'Štítok', + 'tags' => 'Štítky', + 'tag_value' => 'Hodnota štítku (Voliteľné)', + 'tags_explain' => "Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.", + 'tags_add' => 'Pridať ďalší štítok', + 'attachments' => 'Prílohy', + 'attachments_explain' => 'Nahrajte nejaké súbory alebo priložte zopár odkazov pre zobrazenie na Vašej stránke. Budú viditeľné v bočnom paneli.', + 'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.', + 'attachments_items' => 'Priložené položky', + 'attachments_upload' => 'Nahrať súbor', + 'attachments_link' => 'Priložiť odkaz', + 'attachments_set_link' => 'Nastaviť odkaz', + 'attachments_delete_confirm' => 'Kliknite znova na zmazať pre potvrdenie zmazania prílohy.', + 'attachments_dropzone' => 'Presuňte súbory alebo klinknite sem pre priloženie súboru', + 'attachments_no_files' => 'Žiadne súbory neboli nahrané', + 'attachments_explain_link' => 'Ak nechcete priložiť súbor, môžete priložiť odkaz. Môže to byť odkaz na inú stránku alebo odkaz na súbor v cloude.', + 'attachments_link_name' => 'Názov odkazu', + 'attachment_link' => 'Odkaz na prílohu', + 'attachments_link_url' => 'Odkaz na súbor', + 'attachments_link_url_hint' => 'Url stránky alebo súboru', + 'attach' => 'Priložiť', + 'attachments_edit_file' => 'Upraviť súbor', + 'attachments_edit_file_name' => 'Názov súboru', + 'attachments_edit_drop_upload' => 'Presuňte súbory sem alebo klinknite pre nahranie a prepis', + 'attachments_order_updated' => 'Poradie príloh aktualizované', + 'attachments_updated_success' => 'Detaily prílohy aktualizované', + 'attachments_deleted' => 'Príloha zmazaná', + 'attachments_file_uploaded' => 'Súbor úspešne nahraný', + 'attachments_file_updated' => 'Súbor úspešne aktualizovaný', + 'attachments_link_attached' => 'Odkaz úspešne pripojený k stránke', + + /** + * Profile View + */ + 'profile_user_for_x' => 'Používateľ už :time', + 'profile_created_content' => 'Vytvorený obsah', + 'profile_not_created_pages' => ':userName nevytvoril žiadne stránky', + 'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly', + 'profile_not_created_books' => ':userName nevytvoril žiadne knihy', +]; diff --git a/resources/lang/sk/errors.php b/resources/lang/sk/errors.php new file mode 100644 index 000000000..e3420852a --- /dev/null +++ b/resources/lang/sk/errors.php @@ -0,0 +1,70 @@ + 'Nemáte oprávnenie pre prístup k požadovanej stránke.', + 'permissionJson' => 'Nemáte oprávnenie pre vykonanie požadovaného úkonu.', + + // Auth + 'error_user_exists_different_creds' => 'Používateľ s emailom :email už existuje, ale s inými údajmi.', + 'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.', + 'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.', + 'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.', + 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', + 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', + 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', + 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', + 'social_no_action_defined' => 'Nebola definovaná žiadna akcia', + 'social_account_in_use' => 'Tento :socialAccount účet sa už používa, skúste sa prihlásiť pomocou možnosti :socialAccount.', + 'social_account_email_in_use' => 'Email :email sa už používa. Ak už máte účet, môžete pripojiť svoj :socialAccount účet v nastaveniach profilu.', + 'social_account_existing' => 'Tento :socialAccount účet je už spojený s Vaším profilom.', + 'social_account_already_used_existing' => 'Tento :socialAccount účet už používa iný používateľ.', + 'social_account_not_used' => 'Tento :socialAccount účet nie je spojený so žiadnym používateľom. Pripojte ho prosím v nastaveniach Vášho profilu. ', + 'social_account_register_instructions' => 'Ak zatiaľ nemáte účet, môžete sa registrovať pomocou možnosti :socialAccount.', + 'social_driver_not_found' => 'Ovládač socialnych sietí nebol nájdený', + 'social_driver_not_configured' => 'Nastavenia Vášho :socialAccount účtu nie sú správne.', + + // System + 'path_not_writable' => 'Do cesty :filePath sa nedá nahrávať. Uistite sa, že je zapisovateľná serverom.', + 'cannot_get_image_from_url' => 'Nedá sa získať obrázok z :url', + 'cannot_create_thumbs' => 'Server nedokáže vytvoriť náhľady. Skontrolujte prosím, či máte nainštalované GD rozšírenie PHP.', + 'server_upload_limit' => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.', + 'image_upload_error' => 'Pri nahrávaní obrázka nastala chyba', + + // Attachments + 'attachment_page_mismatch' => 'Page mismatch during attachment update', + + // Pages + 'page_draft_autosave_fail' => 'Koncept nemohol byť uložený. Uistite sa, že máte pripojenie k internetu pre uložením tejto stránky', + + // Entities + 'entity_not_found' => 'Entita nenájdená', + 'book_not_found' => 'Kniha nenájdená', + 'page_not_found' => 'Stránka nenájdená', + 'chapter_not_found' => 'Kapitola nenájdená', + 'selected_book_not_found' => 'Vybraná kniha nebola nájdená', + 'selected_book_chapter_not_found' => 'Vybraná kniha alebo kapitola nebola nájdená', + 'guests_cannot_save_drafts' => 'Hosť nemôže ukladať koncepty', + + // Users + 'users_cannot_delete_only_admin' => 'Nemôžete zmazať posledného správcu', + 'users_cannot_delete_guest' => 'Nemôžete zmazať hosťa', + + // Roles + 'role_cannot_be_edited' => 'Táto rola nemôže byť upravovaná', + 'role_system_cannot_be_deleted' => 'Táto rola je systémová rola a nemôže byť zmazaná', + 'role_registration_default_cannot_delete' => 'Táto rola nemôže byť zmazaná, pretože je nastavená ako prednastavená rola pri registrácii', + + // Error pages + '404_page_not_found' => 'Stránka nenájdená', + 'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.', + 'return_home' => 'Vrátiť sa domov', + 'error_occurred' => 'Nastala chyba', + 'app_down' => ':appName je momentálne nedostupná', + 'back_soon' => 'Čoskoro bude opäť dostupná.', +]; diff --git a/resources/lang/sk/pagination.php b/resources/lang/sk/pagination.php new file mode 100644 index 000000000..8f844f5f4 --- /dev/null +++ b/resources/lang/sk/pagination.php @@ -0,0 +1,19 @@ + '« Predchádzajúca', + 'next' => 'Ďalšia »', + +]; diff --git a/resources/lang/sk/passwords.php b/resources/lang/sk/passwords.php new file mode 100644 index 000000000..ff2eb68fa --- /dev/null +++ b/resources/lang/sk/passwords.php @@ -0,0 +1,22 @@ + 'Heslo musí obsahovať aspoň šesť znakov a musí byť rovnaké ako potvrdzujúce.', + 'user' => "Nenašli sme používateľa s takou emailovou adresou.", + 'token' => 'Tento token pre reset hesla je neplatný.', + 'sent' => 'Poslali sme Vám email s odkazom na reset hesla!', + 'reset' => 'Vaše heslo bolo resetované!', + +]; diff --git a/resources/lang/sk/settings.php b/resources/lang/sk/settings.php new file mode 100644 index 000000000..643b4b8ff --- /dev/null +++ b/resources/lang/sk/settings.php @@ -0,0 +1,111 @@ + 'Nastavenia', + 'settings_save' => 'Uložiť nastavenia', + 'settings_save_success' => 'Nastavenia uložené', + + /** + * App settings + */ + + 'app_settings' => 'Nastavenia aplikácie', + 'app_name' => 'Názov aplikácia', + 'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.', + 'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?', + 'app_public_viewing' => 'Povoliť verejné zobrazenie?', + 'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?', + 'app_secure_images_desc' => 'Kvôli výkonu sú všetky obrázky verejné. Táto možnosť pridá pred URL obrázka náhodný, ťažko uhádnuteľný reťazec. Aby ste zabránili jednoduchému prístupu, uistite sa, že indexy priečinkov nie sú povolené.', + 'app_editor' => 'Editor stránky', + 'app_editor_desc' => 'Vyberte editor, ktorý bude používaný všetkými používateľmi na editáciu stránok.', + 'app_custom_html' => 'Vlastný HTML obsah hlavičky', + 'app_custom_html_desc' => 'Všetok text pridaný sem bude vložený naspodok sekcie na každej stránke. Môže sa to zísť pri zmene štýlu alebo pre pridanie analytického kódu.', + 'app_logo' => 'Logo aplikácie', + 'app_logo_desc' => 'Tento obrázok by mal mať 43px na výšku.
Veľké obrázky budú preškálované na menší rozmer.', + 'app_primary_color' => 'Primárna farba pre aplikáciu', + 'app_primary_color_desc' => 'Toto by mala byť hodnota v hex tvare.
Nechajte prázdne ak chcete použiť prednastavenú farbu.', + + /** + * Registration settings + */ + + 'reg_settings' => 'Nastavenia registrácie', + 'reg_allow' => 'Povoliť registráciu?', + 'reg_default_role' => 'Prednastavená používateľská rola po registrácii', + 'reg_confirm_email' => 'Vyžadovať overenie emailu?', + 'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.', + 'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu', + 'reg_confirm_restrict_domain_desc' => 'Zadajte zoznam domén, pre ktoré chcete povoliť registráciu oddelených čiarkou. Používatelia dostanú email kvôli overeniu adresy predtým ako im bude dovolené používať aplikáciu.
Používatelia si budú môcť po úspešnej registrácii zmeniť svoju emailovú adresu.', + 'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia', + + /** + * Role settings + */ + + 'roles' => 'Roly', + 'role_user_roles' => 'Používateľské roly', + 'role_create' => 'Vytvoriť novú rolu', + 'role_create_success' => 'Rola úspešne vytvorená', + 'role_delete' => 'Zmazať rolu', + 'role_delete_confirm' => 'Toto zmaže rolu menom \':roleName\'.', + 'role_delete_users_assigned' => 'Túto rolu má priradenú :userCount používateľov. Ak chcete premigrovať používateľov z tejto roly, vyberte novú rolu nižšie.', + 'role_delete_no_migration' => "Nemigrovať používateľov", + 'role_delete_sure' => 'Ste si istý, že chcete zmazať túto rolu?', + 'role_delete_success' => 'Rola úspešne zmazaná', + 'role_edit' => 'Upraviť rolu', + 'role_details' => 'Detaily roly', + 'role_name' => 'Názov roly', + 'role_desc' => 'Krátky popis roly', + 'role_system' => 'Systémové oprávnenia', + 'role_manage_users' => 'Spravovať používateľov', + 'role_manage_roles' => 'Spravovať role a oprávnenia rolí', + 'role_manage_entity_permissions' => 'Spravovať všetky oprávnenia kníh, kapitol a stránok', + 'role_manage_own_entity_permissions' => 'Spravovať oprávnenia vlastných kníh, kapitol a stránok', + 'role_manage_settings' => 'Spravovať nastavenia aplikácie', + 'role_asset' => 'Oprávnenia majetku', + 'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.', + 'role_all' => 'Všetko', + 'role_own' => 'Vlastné', + 'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané', + 'role_save' => 'Uložiť rolu', + 'role_update_success' => 'Roly úspešne aktualizované', + 'role_users' => 'Používatelia s touto rolou', + 'role_users_none' => 'Žiadni používatelia nemajú priradenú túto rolu', + + /** + * Users + */ + + 'users' => 'Používatelia', + 'user_profile' => 'Profil používateľa', + 'users_add_new' => 'Pridať nového používateľa', + 'users_search' => 'Hľadať medzi používateľmi', + 'users_role' => 'Používateľské roly', + 'users_external_auth_id' => 'Externé autentifikačné ID', + 'users_password_warning' => 'Pole nižšie vyplňte iba ak chcete zmeniť heslo:', + 'users_system_public' => 'Tento účet reprezentuje každého hosťovského používateľa, ktorý navštívi Vašu inštanciu. Nedá sa pomocou neho prihlásiť a je priradený automaticky.', + 'users_delete' => 'Zmazať používateľa', + 'users_delete_named' => 'Zmazať používateľa :userName', + 'users_delete_warning' => ' Toto úplne odstráni používateľa menom \':userName\' zo systému.', + 'users_delete_confirm' => 'Ste si istý, že chcete zmazať tohoto používateľa?', + 'users_delete_success' => 'Používateľ úspešne zmazaný', + 'users_edit' => 'Upraviť používateľa', + 'users_edit_profile' => 'Upraviť profil', + 'users_edit_success' => 'Používateľ úspešne upravený', + 'users_avatar' => 'Avatar používateľa', + 'users_avatar_desc' => 'Tento obrázok by mal byť štvorec s rozmerom približne 256px.', + 'users_preferred_language' => 'Preferovaný jazyk', + 'users_social_accounts' => 'Sociálne účty', + 'users_social_accounts_info' => 'Tu si môžete pripojiť iné účty pre rýchlejšie a jednoduchšie prihlásenie. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.', + 'users_social_connect' => 'Pripojiť účet', + 'users_social_disconnect' => 'Odpojiť účet', + 'users_social_connected' => ':socialAccount účet bol úspešne pripojený k Vášmu profilu.', + 'users_social_disconnected' => ':socialAccount účet bol úspešne odpojený od Vášho profilu.', +]; diff --git a/resources/lang/sk/validation.php b/resources/lang/sk/validation.php new file mode 100644 index 000000000..b365b409d --- /dev/null +++ b/resources/lang/sk/validation.php @@ -0,0 +1,108 @@ + ':attribute musí byť akceptovaný.', + 'active_url' => ':attribute nie je platná URL.', + 'after' => ':attribute musí byť dátum po :date.', + 'alpha' => ':attribute môže obsahovať iba písmená.', + 'alpha_dash' => ':attribute môže obsahovať iba písmená, čísla a pomlčky.', + 'alpha_num' => ':attribute môže obsahovať iba písmená a čísla.', + 'array' => ':attribute musí byť pole.', + 'before' => ':attribute musí byť dátum pred :date.', + 'between' => [ + 'numeric' => ':attribute musí byť medzi :min a :max.', + 'file' => ':attribute musí byť medzi :min a :max kilobajtmi.', + 'string' => ':attribute musí byť medzi :min a :max znakmi.', + 'array' => ':attribute musí byť medzi :min a :max položkami.', + ], + 'boolean' => ':attribute pole musí byť true alebo false.', + 'confirmed' => ':attribute potvrdenie nesedí.', + 'date' => ':attribute nie je platný dátum.', + 'date_format' => ':attribute nesedí s formátom :format.', + 'different' => ':attribute a :other musia byť rozdielne.', + 'digits' => ':attribute musí mať :digits číslic.', + 'digits_between' => ':attribute musí mať medzi :min a :max číslicami.', + 'email' => ':attribute musí byť platná emailová adresa.', + 'filled' => 'Políčko :attribute je povinné.', + 'exists' => 'Vybraný :attribute nie je platný.', + 'image' => ':attribute musí byť obrázok.', + 'in' => 'Vybraný :attribute je neplatný.', + 'integer' => ':attribute musí byť celé číslo.', + 'ip' => ':attribute musí byť platná IP adresa.', + 'max' => [ + 'numeric' => ':attribute nesmie byť väčší ako :max.', + 'file' => ':attribute nesmie byť väčší ako :max kilobajtov.', + 'string' => ':attribute nesmie byť dlhší ako :max znakov.', + 'array' => ':attribute nesmie mať viac ako :max položiek.', + ], + 'mimes' => ':attribute musí byť súbor typu: :values.', + 'min' => [ + 'numeric' => ':attribute musí byť aspoň :min.', + 'file' => ':attribute musí mať aspoň :min kilobajtov.', + 'string' => ':attribute musí mať aspoň :min znakov.', + 'array' => ':attribute musí mať aspoň :min položiek.', + ], + 'not_in' => 'Vybraný :attribute je neplatný.', + 'numeric' => ':attribute musí byť číslo.', + 'regex' => ':attribute formát je neplatný.', + 'required' => 'Políčko :attribute je povinné.', + 'required_if' => 'Políčko :attribute je povinné ak :other je :value.', + 'required_with' => 'Políčko :attribute je povinné ak :values existuje.', + 'required_with_all' => 'Políčko :attribute je povinné ak :values existuje.', + 'required_without' => 'Políčko :attribute je povinné aj :values neexistuje.', + 'required_without_all' => 'Políčko :attribute je povinné ak ani jedno z :values neexistuje.', + 'same' => ':attribute a :other musia byť rovnaké.', + 'size' => [ + 'numeric' => ':attribute musí byť :size.', + 'file' => ':attribute musí mať :size kilobajtov.', + 'string' => ':attribute musí mať :size znakov.', + 'array' => ':attribute musí obsahovať :size položiek.', + ], + 'string' => ':attribute musí byť reťazec.', + 'timezone' => ':attribute musí byť plantá časová zóna.', + 'unique' => ':attribute je už použité.', + 'url' => ':attribute formát je neplatný.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'password-confirm' => [ + 'required_with' => 'Vyžaduje sa potvrdenie hesla', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; From 29a4110d8f0fe70927c93d7b7fb6aabc97217919 Mon Sep 17 00:00:00 2001 From: Abijeet Date: Thu, 13 Apr 2017 23:57:57 +0530 Subject: [PATCH 10/19] Fixes #354, Adds the spellchecker option Uses the browser_spellchecker option documented here - https://www.tinymce.com/docs/configure/spelling/#browser_spellcheck --- resources/assets/js/pages/page-form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 0f44b3d09..2ad934cd9 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -68,6 +68,7 @@ export default function() { window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css') ], body_class: 'page-content', + browser_spellcheck: true, relative_urls: false, remove_script_host: false, document_base_url: window.baseUrl('/'), From ad125327c040830596736f9ac48f97763836318d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 14 Apr 2017 18:47:33 +0100 Subject: [PATCH 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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 16/19] 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 @@ Date: Sun, 16 Apr 2017 16:46:55 +0100 Subject: [PATCH 17/19] Added fade effect to page content highlighting Closes #314 --- resources/assets/js/pages/page-show.js | 6 ++++++ resources/assets/sass/_animations.scss | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index cc6296434..4a4724b85 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -81,6 +81,12 @@ let setupPageShow = window.setupPageShow = function (pageId) { let $idElem = $(idElem); let color = $('#custom-styles').attr('data-color-light'); $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo(); + setTimeout(() => { + $idElem.addClass('anim').addClass('selectFade').css('background-color', ''); + setTimeout(() => { + $idElem.removeClass('selectFade'); + }, 3000); + }, 100); } else { $('.page-content').find(':contains("' + text + '")').smoothScrollTo(); } diff --git a/resources/assets/sass/_animations.scss b/resources/assets/sass/_animations.scss index afcf01cff..467399a66 100644 --- a/resources/assets/sass/_animations.scss +++ b/resources/assets/sass/_animations.scss @@ -126,4 +126,8 @@ animation-duration: 180ms; animation-delay: 0s; animation-timing-function: cubic-bezier(.62, .28, .23, .99); +} + +.anim.selectFade { + transition: background-color ease-in-out 3000ms; } \ No newline at end of file From 746a760a23a123bbf13f86f8d45fe240981da495 Mon Sep 17 00:00:00 2001 From: solidnerd Date: Mon, 17 Apr 2017 09:55:11 +0200 Subject: [PATCH 18/19] Add APP_LOGGING This will add an variable for logging types to make it easier to define outside via .env. Signed-off-by: solidnerd --- config/app.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/app.php b/config/app.php index bf4f84fc7..e70724dce 100644 --- a/config/app.php +++ b/config/app.php @@ -100,7 +100,7 @@ return [ | */ - 'log' => 'single', + 'log' => env('APP_LOGGING', 'single'), /* |-------------------------------------------------------------------------- From fde970ba59b0cbcb40a8c56c50d0bb23720b161f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 17 Apr 2017 12:21:10 +0100 Subject: [PATCH 19/19] Switched out markdown render Fixes #304. Fixes #359. --- package.json | 3 ++- readme.md | 2 +- resources/assets/js/directives.js | 23 +++++++---------------- resources/assets/sass/_text.scss | 18 +++++++++++++++--- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 9f2ce4c1a..b60facff3 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "clipboard": "^1.5.16", "dropzone": "^4.0.1", "gulp-util": "^3.0.8", - "marked": "^0.3.5", + "markdown-it": "^8.3.1", + "markdown-it-task-lists": "^2.0.0", "moment": "^2.12.0", "vue": "^2.2.6" }, diff --git a/readme.md b/readme.md index 65dcbe7b1..3e269e175 100644 --- a/readme.md +++ b/readme.md @@ -74,7 +74,7 @@ These are the great projects used to help build BookStack: * [Dropzone.js](http://www.dropzonejs.com/) * [ZeroClipboard](http://zeroclipboard.org/) * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) -* [Marked](https://github.com/chjj/marked) +* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) * [Moment.js](http://momentjs.com/) * [BarryVD](https://github.com/barryvdh) * [Debugbar](https://github.com/barryvdh/laravel-debugbar) diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 19badcac8..0bc664200 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,6 +1,7 @@ "use strict"; const DropZone = require("dropzone"); -const markdown = require("marked"); +const MarkdownIt = require("markdown-it"); +const mdTasksLists = require('markdown-it-task-lists'); module.exports = function (ngApp, events) { @@ -214,18 +215,8 @@ module.exports = function (ngApp, events) { } }]); - let renderer = new markdown.Renderer(); - // Custom markdown checkbox list item - // Attribution: https://github.com/chjj/marked/issues/107#issuecomment-44542001 - renderer.listitem = function(text) { - if (/^\s*\[[x ]\]\s*/.test(text)) { - text = text - .replace(/^\s*\[ \]\s*/, '') - .replace(/^\s*\[x\]\s*/, ''); - return `
  • ${text}
  • `; - } - return `
  • ${text}
  • `; - }; + const md = new MarkdownIt(); + md.use(mdTasksLists, {label: true}); /** * Markdown input @@ -244,20 +235,20 @@ module.exports = function (ngApp, events) { element = element.find('textarea').first(); let content = element.val(); scope.mdModel = content; - scope.mdChange(markdown(content, {renderer: renderer})); + scope.mdChange(md.render(content)); element.on('change input', (event) => { content = element.val(); $timeout(() => { scope.mdModel = content; - scope.mdChange(markdown(content, {renderer: renderer})); + scope.mdChange(md.render(content)); }); }); scope.$on('markdown-update', (event, value) => { element.val(value); scope.mdModel = value; - scope.mdChange(markdown(value)); + scope.mdChange(md.render(value)); }); } diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index a74a81647..df717dd8d 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -269,19 +269,31 @@ span.highlight { /* * Lists */ +ul, ol { + overflow: hidden; + p { + margin: 0; + } +} ul { padding-left: $-m * 1.3; list-style: disc; - overflow: hidden; + ul { + list-style: circle; + margin-top: 0; + margin-bottom: 0; + } + label { + margin: 0; + } } ol { list-style: decimal; padding-left: $-m * 2; - overflow: hidden; } -li.checkbox-item { +li.checkbox-item, li.task-list-item { list-style: none; margin-left: - ($-m * 1.3); input[type="checkbox"] {
    value) colspan="2" @endif>{{ $tag->name }}{{$tag->value}}value) colspan="2" @endif>{{ $tag->name }}{{$tag->value}}