Dan Brown

Merge branch 'master' into release

Showing 78 changed files with 2757 additions and 908 deletions
...@@ -56,4 +56,13 @@ class Book extends Entity ...@@ -56,4 +56,13 @@ class Book extends Entity
56 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; 56 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
57 } 57 }
58 58
59 + /**
60 + * Return a generalised, common raw query that can be 'unioned' across entities.
61 + * @return string
62 + */
63 + public function entityRawQuery()
64 + {
65 + return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
66 + }
67 +
59 } 68 }
......
...@@ -51,4 +51,13 @@ class Chapter extends Entity ...@@ -51,4 +51,13 @@ class Chapter extends Entity
51 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; 51 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
52 } 52 }
53 53
54 + /**
55 + * Return a generalised, common raw query that can be 'unioned' across entities.
56 + * @return string
57 + */
58 + public function entityRawQuery()
59 + {
60 + return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
61 + }
62 +
54 } 63 }
......
...@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command ...@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
12 * 12 *
13 * @var string 13 * @var string
14 */ 14 */
15 - protected $signature = 'bookstack:regenerate-permissions'; 15 + protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
16 16
17 /** 17 /**
18 * The console command description. 18 * The console command description.
...@@ -46,7 +46,14 @@ class RegeneratePermissions extends Command ...@@ -46,7 +46,14 @@ class RegeneratePermissions extends Command
46 */ 46 */
47 public function handle() 47 public function handle()
48 { 48 {
49 + $connection = \DB::getDefaultConnection();
50 + if ($this->option('database') !== null) {
51 + \DB::setDefaultConnection($this->option('database'));
52 + }
53 +
49 $this->permissionService->buildJointPermissions(); 54 $this->permissionService->buildJointPermissions();
55 +
56 + \DB::setDefaultConnection($connection);
50 $this->comment('Permissions regenerated'); 57 $this->comment('Permissions regenerated');
51 } 58 }
52 } 59 }
......
1 +<?php
2 +
3 +namespace BookStack\Console\Commands;
4 +
5 +use BookStack\Services\SearchService;
6 +use Illuminate\Console\Command;
7 +
8 +class RegenerateSearch extends Command
9 +{
10 + /**
11 + * The name and signature of the console command.
12 + *
13 + * @var string
14 + */
15 + protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
16 +
17 + /**
18 + * The console command description.
19 + *
20 + * @var string
21 + */
22 + protected $description = 'Command description';
23 +
24 + protected $searchService;
25 +
26 + /**
27 + * Create a new command instance.
28 + *
29 + * @param SearchService $searchService
30 + */
31 + public function __construct(SearchService $searchService)
32 + {
33 + parent::__construct();
34 + $this->searchService = $searchService;
35 + }
36 +
37 + /**
38 + * Execute the console command.
39 + *
40 + * @return mixed
41 + */
42 + public function handle()
43 + {
44 + $connection = \DB::getDefaultConnection();
45 + if ($this->option('database') !== null) {
46 + \DB::setDefaultConnection($this->option('database'));
47 + }
48 +
49 + $this->searchService->indexAllEntities();
50 + \DB::setDefaultConnection($connection);
51 + $this->comment('Search index regenerated');
52 + }
53 +}
1 -<?php 1 +<?php namespace BookStack\Console;
2 -
3 -namespace BookStack\Console;
4 2
5 use Illuminate\Console\Scheduling\Schedule; 3 use Illuminate\Console\Scheduling\Schedule;
6 use Illuminate\Foundation\Console\Kernel as ConsoleKernel; 4 use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
...@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel ...@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel
13 * @var array 11 * @var array
14 */ 12 */
15 protected $commands = [ 13 protected $commands = [
16 - \BookStack\Console\Commands\ClearViews::class, 14 + Commands\ClearViews::class,
17 - \BookStack\Console\Commands\ClearActivity::class, 15 + Commands\ClearActivity::class,
18 - \BookStack\Console\Commands\ClearRevisions::class, 16 + Commands\ClearRevisions::class,
19 - \BookStack\Console\Commands\RegeneratePermissions::class, 17 + Commands\RegeneratePermissions::class,
18 + Commands\RegenerateSearch::class
20 ]; 19 ];
21 20
22 /** 21 /**
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 class Entity extends Ownable 4 class Entity extends Ownable
5 { 5 {
6 6
7 - protected $fieldsToSearch = ['name', 'description']; 7 + public $textField = 'description';
8 8
9 /** 9 /**
10 * Compares this entity to another given entity. 10 * Compares this entity to another given entity.
...@@ -66,6 +66,15 @@ class Entity extends Ownable ...@@ -66,6 +66,15 @@ class Entity extends Ownable
66 } 66 }
67 67
68 /** 68 /**
69 + * Get the related search terms.
70 + * @return \Illuminate\Database\Eloquent\Relations\MorphMany
71 + */
72 + public function searchTerms()
73 + {
74 + return $this->morphMany(SearchTerm::class, 'entity');
75 + }
76 +
77 + /**
69 * Get this entities restrictions. 78 * Get this entities restrictions.
70 */ 79 */
71 public function permissions() 80 public function permissions()
...@@ -153,67 +162,19 @@ class Entity extends Ownable ...@@ -153,67 +162,19 @@ class Entity extends Ownable
153 } 162 }
154 163
155 /** 164 /**
156 - * Perform a full-text search on this entity. 165 + * Get the body text of this entity.
157 - * @param string[] $fieldsToSearch
158 - * @param string[] $terms
159 - * @param string[] array $wheres
160 * @return mixed 166 * @return mixed
161 */ 167 */
162 - public function fullTextSearchQuery($terms, $wheres = []) 168 + public function getText()
163 { 169 {
164 - $exactTerms = []; 170 + return $this->{$this->textField};
165 - $fuzzyTerms = []; 171 + }
166 - $search = static::newQuery();
167 -
168 - foreach ($terms as $key => $term) {
169 - $term = htmlentities($term, ENT_QUOTES);
170 - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
171 - if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
172 - $term = str_replace('&quot;', '', $term);
173 - $exactTerms[] = '%' . $term . '%';
174 - } else {
175 - $term = '' . $term . '*';
176 - if ($term !== '*') $fuzzyTerms[] = $term;
177 - }
178 - }
179 -
180 - $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
181 -
182 -
183 - // Perform fulltext search if relevant terms exist.
184 - if ($isFuzzy) {
185 - $termString = implode(' ', $fuzzyTerms);
186 - $fields = implode(',', $this->fieldsToSearch);
187 - $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
188 - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
189 - }
190 -
191 - // Ensure at least one exact term matches if in search
192 - if (count($exactTerms) > 0) {
193 - $search = $search->where(function ($query) use ($exactTerms) {
194 - foreach ($exactTerms as $exactTerm) {
195 - foreach ($this->fieldsToSearch as $field) {
196 - $query->orWhere($field, 'like', $exactTerm);
197 - }
198 - }
199 - });
200 - }
201 -
202 - $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
203 -
204 - // Add additional where terms
205 - foreach ($wheres as $whereTerm) {
206 - $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
207 - }
208 172
209 - // Load in relations 173 + /**
210 - if ($this->isA('page')) { 174 + * Return a generalised, common raw query that can be 'unioned' across entities.
211 - $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); 175 + * @return string
212 - } else if ($this->isA('chapter')) { 176 + */
213 - $search = $search->with('book'); 177 + public function entityRawQuery(){return '';}
214 - }
215 178
216 - return $search->orderBy($orderBy, 'desc');
217 - }
218 179
219 } 180 }
......
1 <?php namespace BookStack\Http\Controllers; 1 <?php namespace BookStack\Http\Controllers;
2 2
3 use BookStack\Repos\EntityRepo; 3 use BookStack\Repos\EntityRepo;
4 +use BookStack\Services\SearchService;
4 use BookStack\Services\ViewService; 5 use BookStack\Services\ViewService;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 7
...@@ -8,16 +9,19 @@ class SearchController extends Controller ...@@ -8,16 +9,19 @@ class SearchController extends Controller
8 { 9 {
9 protected $entityRepo; 10 protected $entityRepo;
10 protected $viewService; 11 protected $viewService;
12 + protected $searchService;
11 13
12 /** 14 /**
13 * SearchController constructor. 15 * SearchController constructor.
14 * @param EntityRepo $entityRepo 16 * @param EntityRepo $entityRepo
15 * @param ViewService $viewService 17 * @param ViewService $viewService
18 + * @param SearchService $searchService
16 */ 19 */
17 - public function __construct(EntityRepo $entityRepo, ViewService $viewService) 20 + public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
18 { 21 {
19 $this->entityRepo = $entityRepo; 22 $this->entityRepo = $entityRepo;
20 $this->viewService = $viewService; 23 $this->viewService = $viewService;
24 + $this->searchService = $searchService;
21 parent::__construct(); 25 parent::__construct();
22 } 26 }
23 27
...@@ -27,105 +31,55 @@ class SearchController extends Controller ...@@ -27,105 +31,55 @@ class SearchController extends Controller
27 * @return \Illuminate\View\View 31 * @return \Illuminate\View\View
28 * @internal param string $searchTerm 32 * @internal param string $searchTerm
29 */ 33 */
30 - public function searchAll(Request $request) 34 + public function search(Request $request)
31 { 35 {
32 - if (!$request->has('term')) {
33 - return redirect()->back();
34 - }
35 $searchTerm = $request->get('term'); 36 $searchTerm = $request->get('term');
36 - $paginationAppends = $request->only('term');
37 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
38 - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
39 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
40 $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); 37 $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
41 - return view('search/all', [
42 - 'pages' => $pages,
43 - 'books' => $books,
44 - 'chapters' => $chapters,
45 - 'searchTerm' => $searchTerm
46 - ]);
47 - }
48 38
49 - /** 39 + $page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
50 - * Search only the pages in the system. 40 + $nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
51 - * @param Request $request
52 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
53 - */
54 - public function searchPages(Request $request)
55 - {
56 - if (!$request->has('term')) return redirect()->back();
57 41
58 - $searchTerm = $request->get('term'); 42 + $results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
59 - $paginationAppends = $request->only('term'); 43 + $hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
60 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
61 - $this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
62 - return view('search/entity-search-list', [
63 - 'entities' => $pages,
64 - 'title' => trans('entities.search_results_page'),
65 - 'searchTerm' => $searchTerm
66 - ]);
67 - }
68 -
69 - /**
70 - * Search only the chapters in the system.
71 - * @param Request $request
72 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
73 - */
74 - public function searchChapters(Request $request)
75 - {
76 - if (!$request->has('term')) return redirect()->back();
77 44
78 - $searchTerm = $request->get('term'); 45 + return view('search/all', [
79 - $paginationAppends = $request->only('term'); 46 + 'entities' => $results['results'],
80 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends); 47 + 'totalResults' => $results['total'],
81 - $this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm])); 48 + 'searchTerm' => $searchTerm,
82 - return view('search/entity-search-list', [ 49 + 'hasNextPage' => $hasNextPage,
83 - 'entities' => $chapters, 50 + 'nextPageLink' => $nextPageLink
84 - 'title' => trans('entities.search_results_chapter'),
85 - 'searchTerm' => $searchTerm
86 ]); 51 ]);
87 } 52 }
88 53
54 +
89 /** 55 /**
90 - * Search only the books in the system. 56 + * Searches all entities within a book.
91 * @param Request $request 57 * @param Request $request
92 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View 58 + * @param integer $bookId
59 + * @return \Illuminate\View\View
60 + * @internal param string $searchTerm
93 */ 61 */
94 - public function searchBooks(Request $request) 62 + public function searchBook(Request $request, $bookId)
95 { 63 {
96 - if (!$request->has('term')) return redirect()->back(); 64 + $term = $request->get('term', '');
97 - 65 + $results = $this->searchService->searchBook($bookId, $term);
98 - $searchTerm = $request->get('term'); 66 + return view('partials/entity-list', ['entities' => $results]);
99 - $paginationAppends = $request->only('term');
100 - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
101 - $this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
102 - return view('search/entity-search-list', [
103 - 'entities' => $books,
104 - 'title' => trans('entities.search_results_book'),
105 - 'searchTerm' => $searchTerm
106 - ]);
107 } 67 }
108 68
109 /** 69 /**
110 - * Searches all entities within a book. 70 + * Searches all entities within a chapter.
111 * @param Request $request 71 * @param Request $request
112 - * @param integer $bookId 72 + * @param integer $chapterId
113 * @return \Illuminate\View\View 73 * @return \Illuminate\View\View
114 * @internal param string $searchTerm 74 * @internal param string $searchTerm
115 */ 75 */
116 - public function searchBook(Request $request, $bookId) 76 + public function searchChapter(Request $request, $chapterId)
117 { 77 {
118 - if (!$request->has('term')) { 78 + $term = $request->get('term', '');
119 - return redirect()->back(); 79 + $results = $this->searchService->searchChapter($chapterId, $term);
120 - } 80 + return view('partials/entity-list', ['entities' => $results]);
121 - $searchTerm = $request->get('term');
122 - $searchWhereTerms = [['book_id', '=', $bookId]];
123 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
124 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
125 - return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
126 } 81 }
127 82
128 -
129 /** 83 /**
130 * Search for a list of entities and return a partial HTML response of matching entities. 84 * Search for a list of entities and return a partial HTML response of matching entities.
131 * Returns the most popular entities if no search is provided. 85 * Returns the most popular entities if no search is provided.
...@@ -134,18 +88,13 @@ class SearchController extends Controller ...@@ -134,18 +88,13 @@ class SearchController extends Controller
134 */ 88 */
135 public function searchEntitiesAjax(Request $request) 89 public function searchEntitiesAjax(Request $request)
136 { 90 {
137 - $entities = collect();
138 $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); 91 $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
139 $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false; 92 $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
140 93
141 // Search for entities otherwise show most popular 94 // Search for entities otherwise show most popular
142 if ($searchTerm !== false) { 95 if ($searchTerm !== false) {
143 - foreach (['page', 'chapter', 'book'] as $entityType) { 96 + $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
144 - if ($entityTypes->contains($entityType)) { 97 + $entities = $this->searchService->searchEntities($searchTerm)['results'];
145 - $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
146 - }
147 - }
148 - $entities = $entities->sortByDesc('title_relevance');
149 } else { 98 } else {
150 $entityNames = $entityTypes->map(function ($type) { 99 $entityNames = $entityTypes->map(function ($type) {
151 return 'BookStack\\' . ucfirst($type); 100 return 'BookStack\\' . ucfirst($type);
......
...@@ -2,12 +2,16 @@ ...@@ -2,12 +2,16 @@
2 2
3 namespace BookStack\Notifications; 3 namespace BookStack\Notifications;
4 4
5 +use Illuminate\Bus\Queueable;
5 use Illuminate\Notifications\Notification; 6 use Illuminate\Notifications\Notification;
7 +use Illuminate\Contracts\Queue\ShouldQueue;
6 use Illuminate\Notifications\Messages\MailMessage; 8 use Illuminate\Notifications\Messages\MailMessage;
7 9
8 -class ConfirmEmail extends Notification 10 +class ConfirmEmail extends Notification implements ShouldQueue
9 { 11 {
10 12
13 + use Queueable;
14 +
11 public $token; 15 public $token;
12 16
13 /** 17 /**
......
...@@ -8,8 +8,7 @@ class Page extends Entity ...@@ -8,8 +8,7 @@ class Page extends Entity
8 protected $simpleAttributes = ['name', 'id', 'slug']; 8 protected $simpleAttributes = ['name', 'id', 'slug'];
9 9
10 protected $with = ['book']; 10 protected $with = ['book'];
11 - 11 + public $textField = 'text';
12 - protected $fieldsToSearch = ['name', 'text'];
13 12
14 /** 13 /**
15 * Converts this page into a simplified array. 14 * Converts this page into a simplified array.
...@@ -96,4 +95,14 @@ class Page extends Entity ...@@ -96,4 +95,14 @@ class Page extends Entity
96 return mb_convert_encoding($text, 'UTF-8'); 95 return mb_convert_encoding($text, 'UTF-8');
97 } 96 }
98 97
98 + /**
99 + * Return a generalised, common raw query that can be 'unioned' across entities.
100 + * @param bool $withContent
101 + * @return string
102 + */
103 + public function entityRawQuery($withContent = false)
104 + { $htmlQuery = $withContent ? 'html' : "'' as html";
105 + return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
106 + }
107 +
99 } 108 }
......
...@@ -8,6 +8,7 @@ use BookStack\Page; ...@@ -8,6 +8,7 @@ use BookStack\Page;
8 use BookStack\PageRevision; 8 use BookStack\PageRevision;
9 use BookStack\Services\AttachmentService; 9 use BookStack\Services\AttachmentService;
10 use BookStack\Services\PermissionService; 10 use BookStack\Services\PermissionService;
11 +use BookStack\Services\SearchService;
11 use BookStack\Services\ViewService; 12 use BookStack\Services\ViewService;
12 use Carbon\Carbon; 13 use Carbon\Carbon;
13 use DOMDocument; 14 use DOMDocument;
...@@ -59,13 +60,12 @@ class EntityRepo ...@@ -59,13 +60,12 @@ class EntityRepo
59 protected $tagRepo; 60 protected $tagRepo;
60 61
61 /** 62 /**
62 - * Acceptable operators to be used in a query 63 + * @var SearchService
63 - * @var array
64 */ 64 */
65 - protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; 65 + protected $searchService;
66 66
67 /** 67 /**
68 - * EntityService constructor. 68 + * EntityRepo constructor.
69 * @param Book $book 69 * @param Book $book
70 * @param Chapter $chapter 70 * @param Chapter $chapter
71 * @param Page $page 71 * @param Page $page
...@@ -73,10 +73,12 @@ class EntityRepo ...@@ -73,10 +73,12 @@ class EntityRepo
73 * @param ViewService $viewService 73 * @param ViewService $viewService
74 * @param PermissionService $permissionService 74 * @param PermissionService $permissionService
75 * @param TagRepo $tagRepo 75 * @param TagRepo $tagRepo
76 + * @param SearchService $searchService
76 */ 77 */
77 public function __construct( 78 public function __construct(
78 Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision, 79 Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
79 - ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo 80 + ViewService $viewService, PermissionService $permissionService,
81 + TagRepo $tagRepo, SearchService $searchService
80 ) 82 )
81 { 83 {
82 $this->book = $book; 84 $this->book = $book;
...@@ -91,6 +93,7 @@ class EntityRepo ...@@ -91,6 +93,7 @@ class EntityRepo
91 $this->viewService = $viewService; 93 $this->viewService = $viewService;
92 $this->permissionService = $permissionService; 94 $this->permissionService = $permissionService;
93 $this->tagRepo = $tagRepo; 95 $this->tagRepo = $tagRepo;
96 + $this->searchService = $searchService;
94 } 97 }
95 98
96 /** 99 /**
...@@ -216,6 +219,7 @@ class EntityRepo ...@@ -216,6 +219,7 @@ class EntityRepo
216 * @param int $count 219 * @param int $count
217 * @param int $page 220 * @param int $page
218 * @param bool|callable $additionalQuery 221 * @param bool|callable $additionalQuery
222 + * @return Collection
219 */ 223 */
220 public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false) 224 public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
221 { 225 {
...@@ -234,6 +238,7 @@ class EntityRepo ...@@ -234,6 +238,7 @@ class EntityRepo
234 * @param int $count 238 * @param int $count
235 * @param int $page 239 * @param int $page
236 * @param bool|callable $additionalQuery 240 * @param bool|callable $additionalQuery
241 + * @return Collection
237 */ 242 */
238 public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false) 243 public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
239 { 244 {
...@@ -327,7 +332,7 @@ class EntityRepo ...@@ -327,7 +332,7 @@ class EntityRepo
327 if ($rawEntity->entity_type === 'BookStack\\Page') { 332 if ($rawEntity->entity_type === 'BookStack\\Page') {
328 $entities[$index] = $this->page->newFromBuilder($rawEntity); 333 $entities[$index] = $this->page->newFromBuilder($rawEntity);
329 if ($renderPages) { 334 if ($renderPages) {
330 - $entities[$index]->html = $rawEntity->description; 335 + $entities[$index]->html = $rawEntity->html;
331 $entities[$index]->html = $this->renderPage($entities[$index]); 336 $entities[$index]->html = $this->renderPage($entities[$index]);
332 }; 337 };
333 } else if ($rawEntity->entity_type === 'BookStack\\Chapter') { 338 } else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
...@@ -343,6 +348,10 @@ class EntityRepo ...@@ -343,6 +348,10 @@ class EntityRepo
343 foreach ($entities as $entity) { 348 foreach ($entities as $entity) {
344 if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue; 349 if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
345 $parentKey = 'BookStack\\Chapter:' . $entity->chapter_id; 350 $parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
351 + if (!isset($parents[$parentKey])) {
352 + $tree[] = $entity;
353 + continue;
354 + }
346 $chapter = $parents[$parentKey]; 355 $chapter = $parents[$parentKey];
347 $chapter->pages->push($entity); 356 $chapter->pages->push($entity);
348 } 357 }
...@@ -354,6 +363,7 @@ class EntityRepo ...@@ -354,6 +363,7 @@ class EntityRepo
354 * Get the child items for a chapter sorted by priority but 363 * Get the child items for a chapter sorted by priority but
355 * with draft items floated to the top. 364 * with draft items floated to the top.
356 * @param Chapter $chapter 365 * @param Chapter $chapter
366 + * @return \Illuminate\Database\Eloquent\Collection|static[]
357 */ 367 */
358 public function getChapterChildren(Chapter $chapter) 368 public function getChapterChildren(Chapter $chapter)
359 { 369 {
...@@ -361,56 +371,6 @@ class EntityRepo ...@@ -361,56 +371,6 @@ class EntityRepo
361 ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get(); 371 ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
362 } 372 }
363 373
364 - /**
365 - * Search entities of a type via a given query.
366 - * @param string $type
367 - * @param string $term
368 - * @param array $whereTerms
369 - * @param int $count
370 - * @param array $paginationAppends
371 - * @return mixed
372 - */
373 - public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
374 - {
375 - $terms = $this->prepareSearchTerms($term);
376 - $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
377 - $q = $this->addAdvancedSearchQueries($q, $term);
378 - $entities = $q->paginate($count)->appends($paginationAppends);
379 - $words = join('|', explode(' ', preg_quote(trim($term), '/')));
380 -
381 - // Highlight page content
382 - if ($type === 'page') {
383 - //lookahead/behind assertions ensures cut between words
384 - $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
385 -
386 - foreach ($entities as $page) {
387 - preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
388 - //delimiter between occurrences
389 - $results = [];
390 - foreach ($matches as $line) {
391 - $results[] = htmlspecialchars($line[0], 0, 'UTF-8');
392 - }
393 - $matchLimit = 6;
394 - if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
395 - $result = join('... ', $results);
396 -
397 - //highlight
398 - $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
399 - if (strlen($result) < 5) $result = $page->getExcerpt(80);
400 -
401 - $page->searchSnippet = $result;
402 - }
403 - return $entities;
404 - }
405 -
406 - // Highlight chapter/book content
407 - foreach ($entities as $entity) {
408 - //highlight
409 - $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
410 - $entity->searchSnippet = $result;
411 - }
412 - return $entities;
413 - }
414 374
415 /** 375 /**
416 * Get the next sequential priority for a new child element in the given book. 376 * Get the next sequential priority for a new child element in the given book.
...@@ -492,104 +452,7 @@ class EntityRepo ...@@ -492,104 +452,7 @@ class EntityRepo
492 $this->permissionService->buildJointPermissionsForEntity($entity); 452 $this->permissionService->buildJointPermissionsForEntity($entity);
493 } 453 }
494 454
495 - /**
496 - * Prepare a string of search terms by turning
497 - * it into an array of terms.
498 - * Keeps quoted terms together.
499 - * @param $termString
500 - * @return array
501 - */
502 - public function prepareSearchTerms($termString)
503 - {
504 - $termString = $this->cleanSearchTermString($termString);
505 - preg_match_all('/(".*?")/', $termString, $matches);
506 - $terms = [];
507 - if (count($matches[1]) > 0) {
508 - foreach ($matches[1] as $match) {
509 - $terms[] = $match;
510 - }
511 - $termString = trim(preg_replace('/"(.*?)"/', '', $termString));
512 - }
513 - if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
514 - return $terms;
515 - }
516 -
517 - /**
518 - * Removes any special search notation that should not
519 - * be used in a full-text search.
520 - * @param $termString
521 - * @return mixed
522 - */
523 - protected function cleanSearchTermString($termString)
524 - {
525 - // Strip tag searches
526 - $termString = preg_replace('/\[.*?\]/', '', $termString);
527 - // Reduced multiple spacing into single spacing
528 - $termString = preg_replace("/\s{2,}/", " ", $termString);
529 - return $termString;
530 - }
531 -
532 - /**
533 - * Get the available query operators as a regex escaped list.
534 - * @return mixed
535 - */
536 - protected function getRegexEscapedOperators()
537 - {
538 - $escapedOperators = [];
539 - foreach ($this->queryOperators as $operator) {
540 - $escapedOperators[] = preg_quote($operator);
541 - }
542 - return join('|', $escapedOperators);
543 - }
544 -
545 - /**
546 - * Parses advanced search notations and adds them to the db query.
547 - * @param $query
548 - * @param $termString
549 - * @return mixed
550 - */
551 - protected function addAdvancedSearchQueries($query, $termString)
552 - {
553 - $escapedOperators = $this->getRegexEscapedOperators();
554 - // Look for tag searches
555 - preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
556 - if (count($tags[0]) > 0) {
557 - $this->applyTagSearches($query, $tags);
558 - }
559 -
560 - return $query;
561 - }
562 455
563 - /**
564 - * Apply extracted tag search terms onto a entity query.
565 - * @param $query
566 - * @param $tags
567 - * @return mixed
568 - */
569 - protected function applyTagSearches($query, $tags) {
570 - $query->where(function($query) use ($tags) {
571 - foreach ($tags[1] as $index => $tagName) {
572 - $query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
573 - $tagOperator = $tags[3][$index];
574 - $tagValue = $tags[4][$index];
575 - if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
576 - if (is_numeric($tagValue) && $tagOperator !== 'like') {
577 - // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
578 - // search the value as a string which prevents being able to do number-based operations
579 - // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
580 - $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
581 - $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
582 - } else {
583 - $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
584 - }
585 - } else {
586 - $query->where('name', '=', $tagName);
587 - }
588 - });
589 - }
590 - });
591 - return $query;
592 - }
593 456
594 /** 457 /**
595 * Create a new entity from request input. 458 * Create a new entity from request input.
...@@ -608,12 +471,13 @@ class EntityRepo ...@@ -608,12 +471,13 @@ class EntityRepo
608 $entity->updated_by = user()->id; 471 $entity->updated_by = user()->id;
609 $isChapter ? $book->chapters()->save($entity) : $entity->save(); 472 $isChapter ? $book->chapters()->save($entity) : $entity->save();
610 $this->permissionService->buildJointPermissionsForEntity($entity); 473 $this->permissionService->buildJointPermissionsForEntity($entity);
474 + $this->searchService->indexEntity($entity);
611 return $entity; 475 return $entity;
612 } 476 }
613 477
614 /** 478 /**
615 * Update entity details from request input. 479 * Update entity details from request input.
616 - * Use for books and chapters 480 + * Used for books and chapters
617 * @param string $type 481 * @param string $type
618 * @param Entity $entityModel 482 * @param Entity $entityModel
619 * @param array $input 483 * @param array $input
...@@ -628,6 +492,7 @@ class EntityRepo ...@@ -628,6 +492,7 @@ class EntityRepo
628 $entityModel->updated_by = user()->id; 492 $entityModel->updated_by = user()->id;
629 $entityModel->save(); 493 $entityModel->save();
630 $this->permissionService->buildJointPermissionsForEntity($entityModel); 494 $this->permissionService->buildJointPermissionsForEntity($entityModel);
495 + $this->searchService->indexEntity($entityModel);
631 return $entityModel; 496 return $entityModel;
632 } 497 }
633 498
...@@ -708,10 +573,11 @@ class EntityRepo ...@@ -708,10 +573,11 @@ class EntityRepo
708 $draftPage->html = $this->formatHtml($input['html']); 573 $draftPage->html = $this->formatHtml($input['html']);
709 $draftPage->text = strip_tags($draftPage->html); 574 $draftPage->text = strip_tags($draftPage->html);
710 $draftPage->draft = false; 575 $draftPage->draft = false;
576 + $draftPage->revision_count = 1;
711 577
712 $draftPage->save(); 578 $draftPage->save();
713 $this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); 579 $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
714 - 580 + $this->searchService->indexEntity($draftPage);
715 return $draftPage; 581 return $draftPage;
716 } 582 }
717 583
...@@ -732,6 +598,7 @@ class EntityRepo ...@@ -732,6 +598,7 @@ class EntityRepo
732 $revision->created_at = $page->updated_at; 598 $revision->created_at = $page->updated_at;
733 $revision->type = 'version'; 599 $revision->type = 'version';
734 $revision->summary = $summary; 600 $revision->summary = $summary;
601 + $revision->revision_number = $page->revision_count;
735 $revision->save(); 602 $revision->save();
736 603
737 // Clear old revisions 604 // Clear old revisions
...@@ -951,6 +818,7 @@ class EntityRepo ...@@ -951,6 +818,7 @@ class EntityRepo
951 $page->text = strip_tags($page->html); 818 $page->text = strip_tags($page->html);
952 if (setting('app-editor') !== 'markdown') $page->markdown = ''; 819 if (setting('app-editor') !== 'markdown') $page->markdown = '';
953 $page->updated_by = $userId; 820 $page->updated_by = $userId;
821 + $page->revision_count++;
954 $page->save(); 822 $page->save();
955 823
956 // Remove all update drafts for this user & page. 824 // Remove all update drafts for this user & page.
...@@ -961,6 +829,8 @@ class EntityRepo ...@@ -961,6 +829,8 @@ class EntityRepo
961 $this->savePageRevision($page, $input['summary']); 829 $this->savePageRevision($page, $input['summary']);
962 } 830 }
963 831
832 + $this->searchService->indexEntity($page);
833 +
964 return $page; 834 return $page;
965 } 835 }
966 836
...@@ -1057,6 +927,7 @@ class EntityRepo ...@@ -1057,6 +927,7 @@ class EntityRepo
1057 */ 927 */
1058 public function restorePageRevision(Page $page, Book $book, $revisionId) 928 public function restorePageRevision(Page $page, Book $book, $revisionId)
1059 { 929 {
930 + $page->revision_count++;
1060 $this->savePageRevision($page); 931 $this->savePageRevision($page);
1061 $revision = $page->revisions()->where('id', '=', $revisionId)->first(); 932 $revision = $page->revisions()->where('id', '=', $revisionId)->first();
1062 $page->fill($revision->toArray()); 933 $page->fill($revision->toArray());
...@@ -1064,6 +935,7 @@ class EntityRepo ...@@ -1064,6 +935,7 @@ class EntityRepo
1064 $page->text = strip_tags($page->html); 935 $page->text = strip_tags($page->html);
1065 $page->updated_by = user()->id; 936 $page->updated_by = user()->id;
1066 $page->save(); 937 $page->save();
938 + $this->searchService->indexEntity($page);
1067 return $page; 939 return $page;
1068 } 940 }
1069 941
...@@ -1156,6 +1028,7 @@ class EntityRepo ...@@ -1156,6 +1028,7 @@ class EntityRepo
1156 $book->views()->delete(); 1028 $book->views()->delete();
1157 $book->permissions()->delete(); 1029 $book->permissions()->delete();
1158 $this->permissionService->deleteJointPermissionsForEntity($book); 1030 $this->permissionService->deleteJointPermissionsForEntity($book);
1031 + $this->searchService->deleteEntityTerms($book);
1159 $book->delete(); 1032 $book->delete();
1160 } 1033 }
1161 1034
...@@ -1175,6 +1048,7 @@ class EntityRepo ...@@ -1175,6 +1048,7 @@ class EntityRepo
1175 $chapter->views()->delete(); 1048 $chapter->views()->delete();
1176 $chapter->permissions()->delete(); 1049 $chapter->permissions()->delete();
1177 $this->permissionService->deleteJointPermissionsForEntity($chapter); 1050 $this->permissionService->deleteJointPermissionsForEntity($chapter);
1051 + $this->searchService->deleteEntityTerms($chapter);
1178 $chapter->delete(); 1052 $chapter->delete();
1179 } 1053 }
1180 1054
...@@ -1190,6 +1064,7 @@ class EntityRepo ...@@ -1190,6 +1064,7 @@ class EntityRepo
1190 $page->revisions()->delete(); 1064 $page->revisions()->delete();
1191 $page->permissions()->delete(); 1065 $page->permissions()->delete();
1192 $this->permissionService->deleteJointPermissionsForEntity($page); 1066 $this->permissionService->deleteJointPermissionsForEntity($page);
1067 + $this->searchService->deleteEntityTerms($page);
1193 1068
1194 // Delete Attached Files 1069 // Delete Attached Files
1195 $attachmentService = app(AttachmentService::class); 1070 $attachmentService = app(AttachmentService::class);
......
1 +<?php namespace BookStack;
2 +
3 +class SearchTerm extends Model
4 +{
5 +
6 + protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
7 + public $timestamps = false;
8 +
9 + /**
10 + * Get the entity that this term belongs to
11 + * @return \Illuminate\Database\Eloquent\Relations\MorphTo
12 + */
13 + public function entity()
14 + {
15 + return $this->morphTo('entity');
16 + }
17 +
18 +}
...@@ -479,8 +479,7 @@ class PermissionService ...@@ -479,8 +479,7 @@ class PermissionService
479 * @return \Illuminate\Database\Query\Builder 479 * @return \Illuminate\Database\Query\Builder
480 */ 480 */
481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { 481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
482 - $pageContentSelect = $fetchPageContent ? 'html' : "''"; 482 + $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
483 - $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
484 $query->where('draft', '=', 0); 483 $query->where('draft', '=', 0);
485 if (!$filterDrafts) { 484 if (!$filterDrafts) {
486 $query->orWhere(function($query) { 485 $query->orWhere(function($query) {
...@@ -488,7 +487,7 @@ class PermissionService ...@@ -488,7 +487,7 @@ class PermissionService
488 }); 487 });
489 } 488 }
490 }); 489 });
491 - $chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id); 490 + $chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
492 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) 491 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
493 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect); 492 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
494 493
...@@ -514,7 +513,7 @@ class PermissionService ...@@ -514,7 +513,7 @@ class PermissionService
514 * @param string $entityType 513 * @param string $entityType
515 * @param Builder|Entity $query 514 * @param Builder|Entity $query
516 * @param string $action 515 * @param string $action
517 - * @return mixed 516 + * @return Builder
518 */ 517 */
519 public function enforceEntityRestrictions($entityType, $query, $action = 'view') 518 public function enforceEntityRestrictions($entityType, $query, $action = 'view')
520 { 519 {
...@@ -540,7 +539,7 @@ class PermissionService ...@@ -540,7 +539,7 @@ class PermissionService
540 } 539 }
541 540
542 /** 541 /**
543 - * Filter items that have entities set a a polymorphic relation. 542 + * Filter items that have entities set as a polymorphic relation.
544 * @param $query 543 * @param $query
545 * @param string $tableName 544 * @param string $tableName
546 * @param string $entityIdColumn 545 * @param string $entityIdColumn
......
1 +<?php namespace BookStack\Services;
2 +
3 +use BookStack\Book;
4 +use BookStack\Chapter;
5 +use BookStack\Entity;
6 +use BookStack\Page;
7 +use BookStack\SearchTerm;
8 +use Illuminate\Database\Connection;
9 +use Illuminate\Database\Query\Builder;
10 +use Illuminate\Database\Query\JoinClause;
11 +use Illuminate\Support\Collection;
12 +
13 +class SearchService
14 +{
15 + protected $searchTerm;
16 + protected $book;
17 + protected $chapter;
18 + protected $page;
19 + protected $db;
20 + protected $permissionService;
21 + protected $entities;
22 +
23 + /**
24 + * Acceptable operators to be used in a query
25 + * @var array
26 + */
27 + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
28 +
29 + /**
30 + * SearchService constructor.
31 + * @param SearchTerm $searchTerm
32 + * @param Book $book
33 + * @param Chapter $chapter
34 + * @param Page $page
35 + * @param Connection $db
36 + * @param PermissionService $permissionService
37 + */
38 + public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
39 + {
40 + $this->searchTerm = $searchTerm;
41 + $this->book = $book;
42 + $this->chapter = $chapter;
43 + $this->page = $page;
44 + $this->db = $db;
45 + $this->entities = [
46 + 'page' => $this->page,
47 + 'chapter' => $this->chapter,
48 + 'book' => $this->book
49 + ];
50 + $this->permissionService = $permissionService;
51 + }
52 +
53 + /**
54 + * Search all entities in the system.
55 + * @param string $searchString
56 + * @param string $entityType
57 + * @param int $page
58 + * @param int $count
59 + * @return array[int, Collection];
60 + */
61 + public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
62 + {
63 + $terms = $this->parseSearchString($searchString);
64 + $entityTypes = array_keys($this->entities);
65 + $entityTypesToSearch = $entityTypes;
66 + $results = collect();
67 +
68 + if ($entityType !== 'all') {
69 + $entityTypesToSearch = $entityType;
70 + } else if (isset($terms['filters']['type'])) {
71 + $entityTypesToSearch = explode('|', $terms['filters']['type']);
72 + }
73 +
74 + $total = 0;
75 +
76 + foreach ($entityTypesToSearch as $entityType) {
77 + if (!in_array($entityType, $entityTypes)) continue;
78 + $search = $this->searchEntityTable($terms, $entityType, $page, $count);
79 + $total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
80 + $results = $results->merge($search);
81 + }
82 +
83 + return [
84 + 'total' => $total,
85 + 'count' => count($results),
86 + 'results' => $results->sortByDesc('score')
87 + ];
88 + }
89 +
90 +
91 + /**
92 + * Search a book for entities
93 + * @param integer $bookId
94 + * @param string $searchString
95 + * @return Collection
96 + */
97 + public function searchBook($bookId, $searchString)
98 + {
99 + $terms = $this->parseSearchString($searchString);
100 + $entityTypes = ['page', 'chapter'];
101 + $entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
102 +
103 + $results = collect();
104 + foreach ($entityTypesToSearch as $entityType) {
105 + if (!in_array($entityType, $entityTypes)) continue;
106 + $search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
107 + $results = $results->merge($search);
108 + }
109 + return $results->sortByDesc('score')->take(20);
110 + }
111 +
112 + /**
113 + * Search a book for entities
114 + * @param integer $chapterId
115 + * @param string $searchString
116 + * @return Collection
117 + */
118 + public function searchChapter($chapterId, $searchString)
119 + {
120 + $terms = $this->parseSearchString($searchString);
121 + $pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
122 + return $pages->sortByDesc('score');
123 + }
124 +
125 + /**
126 + * Search across a particular entity type.
127 + * @param array $terms
128 + * @param string $entityType
129 + * @param int $page
130 + * @param int $count
131 + * @param bool $getCount Return the total count of the search
132 + * @return \Illuminate\Database\Eloquent\Collection|int|static[]
133 + */
134 + public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
135 + {
136 + $query = $this->buildEntitySearchQuery($terms, $entityType);
137 + if ($getCount) return $query->count();
138 +
139 + $query = $query->skip(($page-1) * $count)->take($count);
140 + return $query->get();
141 + }
142 +
143 + /**
144 + * Create a search query for an entity
145 + * @param array $terms
146 + * @param string $entityType
147 + * @return \Illuminate\Database\Eloquent\Builder
148 + */
149 + protected function buildEntitySearchQuery($terms, $entityType = 'page')
150 + {
151 + $entity = $this->getEntity($entityType);
152 + $entitySelect = $entity->newQuery();
153 +
154 + // Handle normal search terms
155 + if (count($terms['search']) > 0) {
156 + $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
157 + $subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
158 + $subQuery->where(function(Builder $query) use ($terms) {
159 + foreach ($terms['search'] as $inputTerm) {
160 + $query->orWhere('term', 'like', $inputTerm .'%');
161 + }
162 + })->groupBy('entity_type', 'entity_id');
163 + $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
164 + $join->on('id', '=', 'entity_id');
165 + })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
166 + $entitySelect->mergeBindings($subQuery);
167 + }
168 +
169 + // Handle exact term matching
170 + if (count($terms['exact']) > 0) {
171 + $entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
172 + foreach ($terms['exact'] as $inputTerm) {
173 + $query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
174 + $query->where('name', 'like', '%'.$inputTerm .'%')
175 + ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
176 + });
177 + }
178 + });
179 + }
180 +
181 + // Handle tag searches
182 + foreach ($terms['tags'] as $inputTerm) {
183 + $this->applyTagSearch($entitySelect, $inputTerm);
184 + }
185 +
186 + // Handle filters
187 + foreach ($terms['filters'] as $filterTerm => $filterValue) {
188 + $functionName = camel_case('filter_' . $filterTerm);
189 + if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
190 + }
191 +
192 + return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
193 + }
194 +
195 +
196 + /**
197 + * Parse a search string into components.
198 + * @param $searchString
199 + * @return array
200 + */
201 + protected function parseSearchString($searchString)
202 + {
203 + $terms = [
204 + 'search' => [],
205 + 'exact' => [],
206 + 'tags' => [],
207 + 'filters' => []
208 + ];
209 +
210 + $patterns = [
211 + 'exact' => '/"(.*?)"/',
212 + 'tags' => '/\[(.*?)\]/',
213 + 'filters' => '/\{(.*?)\}/'
214 + ];
215 +
216 + // Parse special terms
217 + foreach ($patterns as $termType => $pattern) {
218 + $matches = [];
219 + preg_match_all($pattern, $searchString, $matches);
220 + if (count($matches) > 0) {
221 + $terms[$termType] = $matches[1];
222 + $searchString = preg_replace($pattern, '', $searchString);
223 + }
224 + }
225 +
226 + // Parse standard terms
227 + foreach (explode(' ', trim($searchString)) as $searchTerm) {
228 + if ($searchTerm !== '') $terms['search'][] = $searchTerm;
229 + }
230 +
231 + // Split filter values out
232 + $splitFilters = [];
233 + foreach ($terms['filters'] as $filter) {
234 + $explodedFilter = explode(':', $filter, 2);
235 + $splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
236 + }
237 + $terms['filters'] = $splitFilters;
238 +
239 + return $terms;
240 + }
241 +
242 + /**
243 + * Get the available query operators as a regex escaped list.
244 + * @return mixed
245 + */
246 + protected function getRegexEscapedOperators()
247 + {
248 + $escapedOperators = [];
249 + foreach ($this->queryOperators as $operator) {
250 + $escapedOperators[] = preg_quote($operator);
251 + }
252 + return join('|', $escapedOperators);
253 + }
254 +
255 + /**
256 + * Apply a tag search term onto a entity query.
257 + * @param \Illuminate\Database\Eloquent\Builder $query
258 + * @param string $tagTerm
259 + * @return mixed
260 + */
261 + protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
262 + preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
263 + $query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
264 + $tagName = $tagSplit[1];
265 + $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
266 + $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
267 + $validOperator = in_array($tagOperator, $this->queryOperators);
268 + if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
269 + if (!empty($tagName)) $query->where('name', '=', $tagName);
270 + if (is_numeric($tagValue) && $tagOperator !== 'like') {
271 + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
272 + // search the value as a string which prevents being able to do number-based operations
273 + // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
274 + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
275 + $query->whereRaw("value ${tagOperator} ${tagValue}");
276 + } else {
277 + $query->where('value', $tagOperator, $tagValue);
278 + }
279 + } else {
280 + $query->where('name', '=', $tagName);
281 + }
282 + });
283 + return $query;
284 + }
285 +
286 + /**
287 + * Get an entity instance via type.
288 + * @param $type
289 + * @return Entity
290 + */
291 + protected function getEntity($type)
292 + {
293 + return $this->entities[strtolower($type)];
294 + }
295 +
296 + /**
297 + * Index the given entity.
298 + * @param Entity $entity
299 + */
300 + public function indexEntity(Entity $entity)
301 + {
302 + $this->deleteEntityTerms($entity);
303 + $nameTerms = $this->generateTermArrayFromText($entity->name, 5);
304 + $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
305 + $terms = array_merge($nameTerms, $bodyTerms);
306 + foreach ($terms as $index => $term) {
307 + $terms[$index]['entity_type'] = $entity->getMorphClass();
308 + $terms[$index]['entity_id'] = $entity->id;
309 + }
310 + $this->searchTerm->newQuery()->insert($terms);
311 + }
312 +
313 + /**
314 + * Index multiple Entities at once
315 + * @param Entity[] $entities
316 + */
317 + protected function indexEntities($entities) {
318 + $terms = [];
319 + foreach ($entities as $entity) {
320 + $nameTerms = $this->generateTermArrayFromText($entity->name, 5);
321 + $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
322 + foreach (array_merge($nameTerms, $bodyTerms) as $term) {
323 + $term['entity_id'] = $entity->id;
324 + $term['entity_type'] = $entity->getMorphClass();
325 + $terms[] = $term;
326 + }
327 + }
328 +
329 + $chunkedTerms = array_chunk($terms, 500);
330 + foreach ($chunkedTerms as $termChunk) {
331 + $this->searchTerm->newQuery()->insert($termChunk);
332 + }
333 + }
334 +
335 + /**
336 + * Delete and re-index the terms for all entities in the system.
337 + */
338 + public function indexAllEntities()
339 + {
340 + $this->searchTerm->truncate();
341 +
342 + // Chunk through all books
343 + $this->book->chunk(1000, function ($books) {
344 + $this->indexEntities($books);
345 + });
346 +
347 + // Chunk through all chapters
348 + $this->chapter->chunk(1000, function ($chapters) {
349 + $this->indexEntities($chapters);
350 + });
351 +
352 + // Chunk through all pages
353 + $this->page->chunk(1000, function ($pages) {
354 + $this->indexEntities($pages);
355 + });
356 + }
357 +
358 + /**
359 + * Delete related Entity search terms.
360 + * @param Entity $entity
361 + */
362 + public function deleteEntityTerms(Entity $entity)
363 + {
364 + $entity->searchTerms()->delete();
365 + }
366 +
367 + /**
368 + * Create a scored term array from the given text.
369 + * @param $text
370 + * @param float|int $scoreAdjustment
371 + * @return array
372 + */
373 + protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
374 + {
375 + $tokenMap = []; // {TextToken => OccurrenceCount}
376 + $splitText = explode(' ', $text);
377 + foreach ($splitText as $token) {
378 + if ($token === '') continue;
379 + if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
380 + $tokenMap[$token]++;
381 + }
382 +
383 + $terms = [];
384 + foreach ($tokenMap as $token => $count) {
385 + $terms[] = [
386 + 'term' => $token,
387 + 'score' => $count * $scoreAdjustment
388 + ];
389 + }
390 + return $terms;
391 + }
392 +
393 +
394 +
395 +
396 + /**
397 + * Custom entity search filters
398 + */
399 +
400 + protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
401 + {
402 + try { $date = date_create($input);
403 + } catch (\Exception $e) {return;}
404 + $query->where('updated_at', '>=', $date);
405 + }
406 +
407 + protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
408 + {
409 + try { $date = date_create($input);
410 + } catch (\Exception $e) {return;}
411 + $query->where('updated_at', '<', $date);
412 + }
413 +
414 + protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
415 + {
416 + try { $date = date_create($input);
417 + } catch (\Exception $e) {return;}
418 + $query->where('created_at', '>=', $date);
419 + }
420 +
421 + protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
422 + {
423 + try { $date = date_create($input);
424 + } catch (\Exception $e) {return;}
425 + $query->where('created_at', '<', $date);
426 + }
427 +
428 + protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
429 + {
430 + if (!is_numeric($input) && $input !== 'me') return;
431 + if ($input === 'me') $input = user()->id;
432 + $query->where('created_by', '=', $input);
433 + }
434 +
435 + protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
436 + {
437 + if (!is_numeric($input) && $input !== 'me') return;
438 + if ($input === 'me') $input = user()->id;
439 + $query->where('updated_by', '=', $input);
440 + }
441 +
442 + protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
443 + {
444 + $query->where('name', 'like', '%' .$input. '%');
445 + }
446 +
447 + protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
448 +
449 + protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
450 + {
451 + $query->where($model->textField, 'like', '%' .$input. '%');
452 + }
453 +
454 + protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
455 + {
456 + $query->where('restricted', '=', true);
457 + }
458 +
459 + protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
460 + {
461 + $query->whereHas('views', function($query) {
462 + $query->where('user_id', '=', user()->id);
463 + });
464 + }
465 +
466 + protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
467 + {
468 + $query->whereDoesntHave('views', function($query) {
469 + $query->where('user_id', '=', user()->id);
470 + });
471 + }
472 +
473 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -64,6 +64,10 @@ ...@@ -64,6 +64,10 @@
64 "post-update-cmd": [ 64 "post-update-cmd": [
65 "Illuminate\\Foundation\\ComposerScripts::postUpdate", 65 "Illuminate\\Foundation\\ComposerScripts::postUpdate",
66 "php artisan optimize" 66 "php artisan optimize"
67 + ],
68 + "refresh-test-database": [
69 + "php artisan migrate:refresh --database=mysql_testing",
70 + "php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
67 ] 71 ]
68 }, 72 },
69 "config": { 73 "config": {
......
...@@ -100,7 +100,7 @@ return [ ...@@ -100,7 +100,7 @@ return [
100 | 100 |
101 */ 101 */
102 102
103 - 'log' => 'single', 103 + 'log' => env('APP_LOGGING', 'single'),
104 104
105 /* 105 /*
106 |-------------------------------------------------------------------------- 106 |--------------------------------------------------------------------------
......
...@@ -43,7 +43,8 @@ $factory->define(BookStack\Page::class, function ($faker) { ...@@ -43,7 +43,8 @@ $factory->define(BookStack\Page::class, function ($faker) {
43 'name' => $faker->sentence, 43 'name' => $faker->sentence,
44 'slug' => str_random(10), 44 'slug' => str_random(10),
45 'html' => $html, 45 'html' => $html,
46 - 'text' => strip_tags($html) 46 + 'text' => strip_tags($html),
47 + 'revision_count' => 1
47 ]; 48 ];
48 }); 49 });
49 50
......
...@@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration ...@@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration
12 */ 12 */
13 public function up() 13 public function up()
14 { 14 {
15 - DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)'); 15 + $prefix = DB::getTablePrefix();
16 - DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)'); 16 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
17 - DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)'); 17 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
18 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
18 } 19 }
19 20
20 /** 21 /**
......
...@@ -12,9 +12,10 @@ class FulltextWeighting extends Migration ...@@ -12,9 +12,10 @@ class FulltextWeighting extends Migration
12 */ 12 */
13 public function up() 13 public function up()
14 { 14 {
15 - DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)'); 15 + $prefix = DB::getTablePrefix();
16 - DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)'); 16 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
17 - DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)'); 17 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
18 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
18 } 19 }
19 20
20 /** 21 /**
......
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class CreateSearchIndexTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::create('search_terms', function (Blueprint $table) {
17 + $table->increments('id');
18 + $table->string('term', 200);
19 + $table->string('entity_type', 100);
20 + $table->integer('entity_id');
21 + $table->integer('score');
22 +
23 + $table->index('term');
24 + $table->index('entity_type');
25 + $table->index(['entity_type', 'entity_id']);
26 + $table->index('score');
27 + });
28 +
29 + // Drop search indexes
30 + Schema::table('pages', function(Blueprint $table) {
31 + $table->dropIndex('search');
32 + $table->dropIndex('name_search');
33 + });
34 + Schema::table('books', function(Blueprint $table) {
35 + $table->dropIndex('search');
36 + $table->dropIndex('name_search');
37 + });
38 + Schema::table('chapters', function(Blueprint $table) {
39 + $table->dropIndex('search');
40 + $table->dropIndex('name_search');
41 + });
42 +
43 + app(\BookStack\Services\SearchService::class)->indexAllEntities();
44 + }
45 +
46 + /**
47 + * Reverse the migrations.
48 + *
49 + * @return void
50 + */
51 + public function down()
52 + {
53 + $prefix = DB::getTablePrefix();
54 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
55 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
56 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
57 + DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
58 + DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
59 + DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
60 +
61 + Schema::dropIfExists('search_terms');
62 + }
63 +}
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class AddRevisionCounts extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::table('pages', function (Blueprint $table) {
17 + $table->integer('revision_count');
18 + });
19 + Schema::table('page_revisions', function (Blueprint $table) {
20 + $table->integer('revision_number');
21 + $table->index('revision_number');
22 + });
23 +
24 + // Update revision count
25 + $pTable = DB::getTablePrefix() . 'pages';
26 + $rTable = DB::getTablePrefix() . 'page_revisions';
27 + DB::statement("UPDATE ${pTable} SET ${pTable}.revision_count=(SELECT count(*) FROM ${rTable} WHERE ${rTable}.page_id=${pTable}.id)");
28 + }
29 +
30 + /**
31 + * Reverse the migrations.
32 + *
33 + * @return void
34 + */
35 + public function down()
36 + {
37 + Schema::table('pages', function (Blueprint $table) {
38 + $table->dropColumn('revision_count');
39 + });
40 + Schema::table('page_revisions', function (Blueprint $table) {
41 + $table->dropColumn('revision_number');
42 + });
43 + }
44 +}
...@@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder ...@@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder
16 $user->attachRole($role); 16 $user->attachRole($role);
17 17
18 18
19 - $books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id]) 19 + factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
20 ->each(function($book) use ($user) { 20 ->each(function($book) use ($user) {
21 $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) 21 $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
22 ->each(function($chapter) use ($user, $book){ 22 ->each(function($chapter) use ($user, $book){
...@@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder ...@@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder
28 $book->pages()->saveMany($pages); 28 $book->pages()->saveMany($pages);
29 }); 29 });
30 30
31 - $restrictionService = app(\BookStack\Services\PermissionService::class); 31 + app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
32 - $restrictionService->buildJointPermissions(); 32 + app(\BookStack\Services\SearchService::class)->indexAllEntities();
33 } 33 }
34 } 34 }
......
1 -var elixir = require('laravel-elixir'); 1 +const argv = require('yargs').argv;
2 +const gulp = require('gulp'),
3 + plumber = require('gulp-plumber');
4 +const autoprefixer = require('gulp-autoprefixer');
5 +const uglify = require('gulp-uglify');
6 +const minifycss = require('gulp-clean-css');
7 +const sass = require('gulp-sass');
8 +const browserify = require("browserify");
9 +const source = require('vinyl-source-stream');
10 +const buffer = require('vinyl-buffer');
11 +const babelify = require("babelify");
12 +const watchify = require("watchify");
13 +const envify = require("envify");
14 +const gutil = require("gulp-util");
2 15
3 -elixir(mix => { 16 +if (argv.production) process.env.NODE_ENV = 'production';
4 - mix.sass('styles.scss'); 17 +
5 - mix.sass('print-styles.scss'); 18 +gulp.task('styles', () => {
6 - mix.sass('export-styles.scss'); 19 + let chain = gulp.src(['resources/assets/sass/**/*.scss'])
7 - mix.browserify('global.js', './public/js/common.js'); 20 + .pipe(plumber({
21 + errorHandler: function (error) {
22 + console.log(error.message);
23 + this.emit('end');
24 + }}))
25 + .pipe(sass())
26 + .pipe(autoprefixer('last 2 versions'));
27 + if (argv.production) chain = chain.pipe(minifycss());
28 + return chain.pipe(gulp.dest('public/css/'));
8 }); 29 });
30 +
31 +
32 +function scriptTask(watch=false) {
33 +
34 + let props = {
35 + basedir: 'resources/assets/js',
36 + debug: true,
37 + entries: ['global.js']
38 + };
39 +
40 + let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props);
41 + bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']});
42 + function rebundle() {
43 + let stream = bundler.bundle();
44 + stream = stream.pipe(source('common.js'));
45 + if (argv.production) stream = stream.pipe(buffer()).pipe(uglify());
46 + return stream.pipe(gulp.dest('public/js/'));
47 + }
48 + bundler.on('update', function() {
49 + rebundle();
50 + gutil.log('Rebundle...');
51 + });
52 + bundler.on('log', gutil.log);
53 + return rebundle();
54 +}
55 +
56 +gulp.task('scripts', () => {scriptTask(false)});
57 +gulp.task('scripts-watch', () => {scriptTask(true)});
58 +
59 +gulp.task('default', ['styles', 'scripts-watch'], () => {
60 + gulp.watch("resources/assets/sass/**/*.scss", ['styles']);
61 +});
62 +
63 +gulp.task('build', ['styles', 'scripts']);
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "private": true, 2 "private": true,
3 "scripts": { 3 "scripts": {
4 - "build": "gulp --production", 4 + "build": "gulp build",
5 - "dev": "gulp watch", 5 + "production": "gulp build --production",
6 - "watch": "gulp watch" 6 + "dev": "gulp",
7 + "watch": "gulp"
7 }, 8 },
8 "devDependencies": { 9 "devDependencies": {
10 + "babelify": "^7.3.0",
11 + "browserify": "^14.3.0",
12 + "envify": "^4.0.0",
13 + "gulp": "3.9.1",
14 + "gulp-autoprefixer": "3.1.1",
15 + "gulp-clean-css": "^3.0.4",
16 + "gulp-minify-css": "1.2.4",
17 + "gulp-plumber": "1.1.0",
18 + "gulp-sass": "3.1.0",
19 + "gulp-uglify": "2.1.2",
20 + "vinyl-buffer": "^1.0.0",
21 + "vinyl-source-stream": "^1.1.0",
22 + "watchify": "^3.9.0",
23 + "yargs": "^7.1.0"
24 + },
25 + "dependencies": {
9 "angular": "^1.5.5", 26 "angular": "^1.5.5",
10 "angular-animate": "^1.5.5", 27 "angular-animate": "^1.5.5",
11 "angular-resource": "^1.5.5", 28 "angular-resource": "^1.5.5",
12 "angular-sanitize": "^1.5.5", 29 "angular-sanitize": "^1.5.5",
13 - "angular-ui-sortable": "^0.15.0", 30 + "angular-ui-sortable": "^0.17.0",
31 + "axios": "^0.16.1",
32 + "babel-preset-es2015": "^6.24.1",
33 + "clipboard": "^1.5.16",
14 "dropzone": "^4.0.1", 34 "dropzone": "^4.0.1",
15 - "gulp": "^3.9.0", 35 + "gulp-util": "^3.0.8",
16 - "laravel-elixir": "^6.0.0-11", 36 + "markdown-it": "^8.3.1",
17 - "laravel-elixir-browserify-official": "^0.1.3", 37 + "markdown-it-task-lists": "^2.0.0",
18 - "marked": "^0.3.5", 38 + "moment": "^2.12.0",
19 - "moment": "^2.12.0" 39 + "vue": "^2.2.6"
20 }, 40 },
21 - "dependencies": { 41 + "browser": {
22 - "clipboard": "^1.5.16" 42 + "vue": "vue/dist/vue.common.js"
23 } 43 }
24 } 44 }
......
...@@ -74,7 +74,7 @@ These are the great projects used to help build BookStack: ...@@ -74,7 +74,7 @@ These are the great projects used to help build BookStack:
74 * [Dropzone.js](http://www.dropzonejs.com/) 74 * [Dropzone.js](http://www.dropzonejs.com/)
75 * [ZeroClipboard](http://zeroclipboard.org/) 75 * [ZeroClipboard](http://zeroclipboard.org/)
76 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) 76 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
77 -* [Marked](https://github.com/chjj/marked) 77 +* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
78 * [Moment.js](http://momentjs.com/) 78 * [Moment.js](http://momentjs.com/)
79 * [BarryVD](https://github.com/barryvdh) 79 * [BarryVD](https://github.com/barryvdh)
80 * [Debugbar](https://github.com/barryvdh/laravel-debugbar) 80 * [Debugbar](https://github.com/barryvdh/laravel-debugbar)
......
1 "use strict"; 1 "use strict";
2 2
3 -import moment from 'moment'; 3 +const moment = require('moment');
4 -import 'moment/locale/en-gb'; 4 +require('moment/locale/en-gb');
5 -import editorOptions from "./pages/page-form"; 5 +const editorOptions = require("./pages/page-form");
6 6
7 moment.locale('en-gb'); 7 moment.locale('en-gb');
8 8
9 -export default function (ngApp, events) { 9 +module.exports = function (ngApp, events) {
10 10
11 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', 11 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
12 function ($scope, $attrs, $http, $timeout, imageManagerService) { 12 function ($scope, $attrs, $http, $timeout, imageManagerService) {
...@@ -259,39 +259,6 @@ export default function (ngApp, events) { ...@@ -259,39 +259,6 @@ export default function (ngApp, events) {
259 259
260 }]); 260 }]);
261 261
262 -
263 - ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
264 - $scope.searching = false;
265 - $scope.searchTerm = '';
266 - $scope.searchResults = '';
267 -
268 - $scope.searchBook = function (e) {
269 - e.preventDefault();
270 - let term = $scope.searchTerm;
271 - if (term.length == 0) return;
272 - $scope.searching = true;
273 - $scope.searchResults = '';
274 - let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
275 - searchUrl += '?term=' + encodeURIComponent(term);
276 - $http.get(searchUrl).then((response) => {
277 - $scope.searchResults = $sce.trustAsHtml(response.data);
278 - });
279 - };
280 -
281 - $scope.checkSearchForm = function () {
282 - if ($scope.searchTerm.length < 1) {
283 - $scope.searching = false;
284 - }
285 - };
286 -
287 - $scope.clearSearch = function () {
288 - $scope.searching = false;
289 - $scope.searchTerm = '';
290 - };
291 -
292 - }]);
293 -
294 -
295 ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', 262 ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
296 function ($scope, $http, $attrs, $interval, $timeout, $sce) { 263 function ($scope, $http, $attrs, $interval, $timeout, $sce) {
297 264
......
1 "use strict"; 1 "use strict";
2 -import DropZone from "dropzone"; 2 +const DropZone = require("dropzone");
3 -import markdown from "marked"; 3 +const MarkdownIt = require("markdown-it");
4 +const mdTasksLists = require('markdown-it-task-lists');
4 5
5 -export default function (ngApp, events) { 6 +module.exports = function (ngApp, events) {
6 7
7 /** 8 /**
8 * Common tab controls using simple jQuery functions. 9 * Common tab controls using simple jQuery functions.
...@@ -214,18 +215,8 @@ export default function (ngApp, events) { ...@@ -214,18 +215,8 @@ export default function (ngApp, events) {
214 } 215 }
215 }]); 216 }]);
216 217
217 - let renderer = new markdown.Renderer(); 218 + const md = new MarkdownIt();
218 - // Custom markdown checkbox list item 219 + md.use(mdTasksLists, {label: true});
219 - // Attribution: https://github.com/chjj/marked/issues/107#issuecomment-44542001
220 - renderer.listitem = function(text) {
221 - if (/^\s*\[[x ]\]\s*/.test(text)) {
222 - text = text
223 - .replace(/^\s*\[ \]\s*/, '<input type="checkbox"/>')
224 - .replace(/^\s*\[x\]\s*/, '<input type="checkbox" checked/>');
225 - return `<li class="checkbox-item">${text}</li>`;
226 - }
227 - return `<li>${text}</li>`;
228 - };
229 220
230 /** 221 /**
231 * Markdown input 222 * Markdown input
...@@ -244,20 +235,20 @@ export default function (ngApp, events) { ...@@ -244,20 +235,20 @@ export default function (ngApp, events) {
244 element = element.find('textarea').first(); 235 element = element.find('textarea').first();
245 let content = element.val(); 236 let content = element.val();
246 scope.mdModel = content; 237 scope.mdModel = content;
247 - scope.mdChange(markdown(content, {renderer: renderer})); 238 + scope.mdChange(md.render(content));
248 239
249 element.on('change input', (event) => { 240 element.on('change input', (event) => {
250 content = element.val(); 241 content = element.val();
251 $timeout(() => { 242 $timeout(() => {
252 scope.mdModel = content; 243 scope.mdModel = content;
253 - scope.mdChange(markdown(content, {renderer: renderer})); 244 + scope.mdChange(md.render(content));
254 }); 245 });
255 }); 246 });
256 247
257 scope.$on('markdown-update', (event, value) => { 248 scope.$on('markdown-update', (event, value) => {
258 element.val(value); 249 element.val(value);
259 scope.mdModel = value; 250 scope.mdModel = value;
260 - scope.mdChange(markdown(value)); 251 + scope.mdChange(md.render(value));
261 }); 252 });
262 253
263 } 254 }
......
1 "use strict"; 1 "use strict";
2 2
3 -// AngularJS - Create application and load components
4 -import angular from "angular";
5 -import "angular-resource";
6 -import "angular-animate";
7 -import "angular-sanitize";
8 -import "angular-ui-sortable";
9 -
10 // Url retrieval function 3 // Url retrieval function
11 window.baseUrl = function(path) { 4 window.baseUrl = function(path) {
12 let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content'); 5 let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
...@@ -15,11 +8,33 @@ window.baseUrl = function(path) { ...@@ -15,11 +8,33 @@ window.baseUrl = function(path) {
15 return basePath + '/' + path; 8 return basePath + '/' + path;
16 }; 9 };
17 10
11 +const Vue = require("vue");
12 +const axios = require("axios");
13 +
14 +let axiosInstance = axios.create({
15 + headers: {
16 + 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
17 + 'baseURL': window.baseUrl('')
18 + }
19 +});
20 +
21 +Vue.prototype.$http = axiosInstance;
22 +
23 +require("./vues/vues");
24 +
25 +
26 +// AngularJS - Create application and load components
27 +const angular = require("angular");
28 +require("angular-resource");
29 +require("angular-animate");
30 +require("angular-sanitize");
31 +require("angular-ui-sortable");
32 +
18 let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); 33 let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
19 34
20 // Translation setup 35 // Translation setup
21 // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system 36 // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
22 -import Translations from "./translations" 37 +const Translations = require("./translations");
23 let translator = new Translations(window.translations); 38 let translator = new Translations(window.translations);
24 window.trans = translator.get.bind(translator); 39 window.trans = translator.get.bind(translator);
25 40
...@@ -47,11 +62,12 @@ class EventManager { ...@@ -47,11 +62,12 @@ class EventManager {
47 } 62 }
48 63
49 window.Events = new EventManager(); 64 window.Events = new EventManager();
65 +Vue.prototype.$events = window.Events;
50 66
51 // Load in angular specific items 67 // Load in angular specific items
52 -import Services from './services'; 68 +const Services = require('./services');
53 -import Directives from './directives'; 69 +const Directives = require('./directives');
54 -import Controllers from './controllers'; 70 +const Controllers = require('./controllers');
55 Services(ngApp, window.Events); 71 Services(ngApp, window.Events);
56 Directives(ngApp, window.Events); 72 Directives(ngApp, window.Events);
57 Controllers(ngApp, window.Events); 73 Controllers(ngApp, window.Events);
...@@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1 ...@@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1
154 } 170 }
155 171
156 // Page specific items 172 // Page specific items
157 -import "./pages/page-show"; 173 +require("./pages/page-show");
......
...@@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) { ...@@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) {
60 editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']); 60 editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
61 } 61 }
62 62
63 -export default function() { 63 +module.exports = function() {
64 let settings = { 64 let settings = {
65 selector: '#html-editor', 65 selector: '#html-editor',
66 content_css: [ 66 content_css: [
...@@ -68,6 +68,7 @@ export default function() { ...@@ -68,6 +68,7 @@ export default function() {
68 window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css') 68 window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
69 ], 69 ],
70 body_class: 'page-content', 70 body_class: 'page-content',
71 + browser_spellcheck: true,
71 relative_urls: false, 72 relative_urls: false,
72 remove_script_host: false, 73 remove_script_host: false,
73 document_base_url: window.baseUrl('/'), 74 document_base_url: window.baseUrl('/'),
...@@ -213,4 +214,4 @@ export default function() { ...@@ -213,4 +214,4 @@ export default function() {
213 } 214 }
214 }; 215 };
215 return settings; 216 return settings;
216 -}
...\ No newline at end of file ...\ No newline at end of file
217 +};
...\ No newline at end of file ...\ No newline at end of file
......
1 "use strict"; 1 "use strict";
2 // Configure ZeroClipboard 2 // Configure ZeroClipboard
3 -import Clipboard from "clipboard"; 3 +const Clipboard = require("clipboard");
4 4
5 -export default window.setupPageShow = function (pageId) { 5 +let setupPageShow = window.setupPageShow = function (pageId) {
6 6
7 // Set up pointer 7 // Set up pointer
8 let $pointer = $('#pointer').detach(); 8 let $pointer = $('#pointer').detach();
...@@ -81,6 +81,12 @@ export default window.setupPageShow = function (pageId) { ...@@ -81,6 +81,12 @@ export default window.setupPageShow = function (pageId) {
81 let $idElem = $(idElem); 81 let $idElem = $(idElem);
82 let color = $('#custom-styles').attr('data-color-light'); 82 let color = $('#custom-styles').attr('data-color-light');
83 $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo(); 83 $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
84 + setTimeout(() => {
85 + $idElem.addClass('anim').addClass('selectFade').css('background-color', '');
86 + setTimeout(() => {
87 + $idElem.removeClass('selectFade');
88 + }, 3000);
89 + }, 100);
84 } else { 90 } else {
85 $('.page-content').find(':contains("' + text + '")').smoothScrollTo(); 91 $('.page-content').find(':contains("' + text + '")').smoothScrollTo();
86 } 92 }
...@@ -151,3 +157,5 @@ export default window.setupPageShow = function (pageId) { ...@@ -151,3 +157,5 @@ export default window.setupPageShow = function (pageId) {
151 }); 157 });
152 158
153 }; 159 };
160 +
161 +module.exports = setupPageShow;
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -44,4 +44,4 @@ class Translator { ...@@ -44,4 +44,4 @@ class Translator {
44 44
45 } 45 }
46 46
47 -export default Translator 47 +module.exports = Translator;
......
1 +let data = {
2 + id: null,
3 + type: '',
4 + searching: false,
5 + searchTerm: '',
6 + searchResults: '',
7 +};
8 +
9 +let computed = {
10 +
11 +};
12 +
13 +let methods = {
14 +
15 + searchBook() {
16 + if (this.searchTerm.trim().length === 0) return;
17 + this.searching = true;
18 + this.searchResults = '';
19 + let url = window.baseUrl(`/search/${this.type}/${this.id}`);
20 + url += `?term=${encodeURIComponent(this.searchTerm)}`;
21 + this.$http.get(url).then(resp => {
22 + this.searchResults = resp.data;
23 + });
24 + },
25 +
26 + checkSearchForm() {
27 + this.searching = this.searchTerm > 0;
28 + },
29 +
30 + clearSearch() {
31 + this.searching = false;
32 + this.searchTerm = '';
33 + }
34 +
35 +};
36 +
37 +function mounted() {
38 + this.id = Number(this.$el.getAttribute('entity-id'));
39 + this.type = this.$el.getAttribute('entity-type');
40 +}
41 +
42 +module.exports = {
43 + data, computed, methods, mounted
44 +};
...\ No newline at end of file ...\ No newline at end of file
1 +const moment = require('moment');
2 +
3 +let data = {
4 + terms: '',
5 + termString : '',
6 + search: {
7 + type: {
8 + page: true,
9 + chapter: true,
10 + book: true
11 + },
12 + exactTerms: [],
13 + tagTerms: [],
14 + option: {},
15 + dates: {
16 + updated_after: false,
17 + updated_before: false,
18 + created_after: false,
19 + created_before: false,
20 + }
21 + }
22 +};
23 +
24 +let computed = {
25 +
26 +};
27 +
28 +let methods = {
29 +
30 + appendTerm(term) {
31 + this.termString += ' ' + term;
32 + this.termString = this.termString.replace(/\s{2,}/g, ' ');
33 + this.termString = this.termString.replace(/^\s+/, '');
34 + this.termString = this.termString.replace(/\s+$/, '');
35 + },
36 +
37 + exactParse(searchString) {
38 + this.search.exactTerms = [];
39 + let exactFilter = /"(.+?)"/g;
40 + let matches;
41 + while ((matches = exactFilter.exec(searchString)) !== null) {
42 + this.search.exactTerms.push(matches[1]);
43 + }
44 + },
45 +
46 + exactChange() {
47 + let exactFilter = /"(.+?)"/g;
48 + this.termString = this.termString.replace(exactFilter, '');
49 + let matchesTerm = this.search.exactTerms.filter(term => {
50 + return term.trim() !== '';
51 + }).map(term => {
52 + return `"${term}"`
53 + }).join(' ');
54 + this.appendTerm(matchesTerm);
55 + },
56 +
57 + addExact() {
58 + this.search.exactTerms.push('');
59 + setTimeout(() => {
60 + let exactInputs = document.querySelectorAll('.exact-input');
61 + exactInputs[exactInputs.length - 1].focus();
62 + }, 100);
63 + },
64 +
65 + removeExact(index) {
66 + this.search.exactTerms.splice(index, 1);
67 + this.exactChange();
68 + },
69 +
70 + tagParse(searchString) {
71 + this.search.tagTerms = [];
72 + let tagFilter = /\[(.+?)\]/g;
73 + let matches;
74 + while ((matches = tagFilter.exec(searchString)) !== null) {
75 + this.search.tagTerms.push(matches[1]);
76 + }
77 + },
78 +
79 + tagChange() {
80 + let tagFilter = /\[(.+?)\]/g;
81 + this.termString = this.termString.replace(tagFilter, '');
82 + let matchesTerm = this.search.tagTerms.filter(term => {
83 + return term.trim() !== '';
84 + }).map(term => {
85 + return `[${term}]`
86 + }).join(' ');
87 + this.appendTerm(matchesTerm);
88 + },
89 +
90 + addTag() {
91 + this.search.tagTerms.push('');
92 + setTimeout(() => {
93 + let tagInputs = document.querySelectorAll('.tag-input');
94 + tagInputs[tagInputs.length - 1].focus();
95 + }, 100);
96 + },
97 +
98 + removeTag(index) {
99 + this.search.tagTerms.splice(index, 1);
100 + this.tagChange();
101 + },
102 +
103 + typeParse(searchString) {
104 + let typeFilter = /{\s?type:\s?(.*?)\s?}/;
105 + let match = searchString.match(typeFilter);
106 + let type = this.search.type;
107 + if (!match) {
108 + type.page = type.book = type.chapter = true;
109 + return;
110 + }
111 + let splitTypes = match[1].replace(/ /g, '').split('|');
112 + type.page = (splitTypes.indexOf('page') !== -1);
113 + type.chapter = (splitTypes.indexOf('chapter') !== -1);
114 + type.book = (splitTypes.indexOf('book') !== -1);
115 + },
116 +
117 + typeChange() {
118 + let typeFilter = /{\s?type:\s?(.*?)\s?}/;
119 + let type = this.search.type;
120 + if (type.page === type.chapter && type.page === type.book) {
121 + this.termString = this.termString.replace(typeFilter, '');
122 + return;
123 + }
124 + let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|');
125 + let typeTerm = '{type:'+selectedTypes+'}';
126 + if (this.termString.match(typeFilter)) {
127 + this.termString = this.termString.replace(typeFilter, typeTerm);
128 + return;
129 + }
130 + this.appendTerm(typeTerm);
131 + },
132 +
133 + optionParse(searchString) {
134 + let optionFilter = /{([a-z_\-:]+?)}/gi;
135 + let matches;
136 + while ((matches = optionFilter.exec(searchString)) !== null) {
137 + this.search.option[matches[1].toLowerCase()] = true;
138 + }
139 + },
140 +
141 + optionChange(optionName) {
142 + let isChecked = this.search.option[optionName];
143 + if (isChecked) {
144 + this.appendTerm(`{${optionName}}`);
145 + } else {
146 + this.termString = this.termString.replace(`{${optionName}}`, '');
147 + }
148 + },
149 +
150 + updateSearch(e) {
151 + e.preventDefault();
152 + window.location = '/search?term=' + encodeURIComponent(this.termString);
153 + },
154 +
155 + enableDate(optionName) {
156 + this.search.dates[optionName.toLowerCase()] = moment().format('YYYY-MM-DD');
157 + this.dateChange(optionName);
158 + },
159 +
160 + dateParse(searchString) {
161 + let dateFilter = /{([a-z_\-]+?):([a-z_\-0-9]+?)}/gi;
162 + let dateTags = Object.keys(this.search.dates);
163 + let matches;
164 + while ((matches = dateFilter.exec(searchString)) !== null) {
165 + if (dateTags.indexOf(matches[1]) === -1) continue;
166 + this.search.dates[matches[1].toLowerCase()] = matches[2];
167 + }
168 + },
169 +
170 + dateChange(optionName) {
171 + let dateFilter = new RegExp('{\\s?'+optionName+'\\s?:([a-z_\\-0-9]+?)}', 'gi');
172 + this.termString = this.termString.replace(dateFilter, '');
173 + if (!this.search.dates[optionName]) return;
174 + this.appendTerm(`{${optionName}:${this.search.dates[optionName]}}`);
175 + },
176 +
177 + dateRemove(optionName) {
178 + this.search.dates[optionName] = false;
179 + this.dateChange(optionName);
180 + }
181 +
182 +};
183 +
184 +function created() {
185 + this.termString = document.querySelector('[name=searchTerm]').value;
186 + this.typeParse(this.termString);
187 + this.exactParse(this.termString);
188 + this.tagParse(this.termString);
189 + this.optionParse(this.termString);
190 + this.dateParse(this.termString);
191 +}
192 +
193 +module.exports = {
194 + data, computed, methods, created
195 +};
...\ No newline at end of file ...\ No newline at end of file
1 +const Vue = require("vue");
2 +
3 +function exists(id) {
4 + return document.getElementById(id) !== null;
5 +}
6 +
7 +let vueMapping = {
8 + 'search-system': require('./search'),
9 + 'entity-dashboard': require('./entity-search'),
10 +};
11 +
12 +Object.keys(vueMapping).forEach(id => {
13 + if (exists(id)) {
14 + let config = vueMapping[id];
15 + config.el = '#' + id;
16 + new Vue(config);
17 + }
18 +});
...\ No newline at end of file ...\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 .anim.fadeIn { 2 .anim.fadeIn {
3 opacity: 0; 3 opacity: 0;
4 animation-name: fadeIn; 4 animation-name: fadeIn;
5 - animation-duration: 160ms; 5 + animation-duration: 180ms;
6 animation-timing-function: ease-in-out; 6 animation-timing-function: ease-in-out;
7 animation-fill-mode: forwards; 7 animation-fill-mode: forwards;
8 } 8 }
...@@ -126,4 +126,8 @@ ...@@ -126,4 +126,8 @@
126 animation-duration: 180ms; 126 animation-duration: 180ms;
127 animation-delay: 0s; 127 animation-delay: 0s;
128 animation-timing-function: cubic-bezier(.62, .28, .23, .99); 128 animation-timing-function: cubic-bezier(.62, .28, .23, .99);
129 +}
130 +
131 +.anim.selectFade {
132 + transition: background-color ease-in-out 3000ms;
129 } 133 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -98,19 +98,36 @@ label { ...@@ -98,19 +98,36 @@ label {
98 98
99 label.radio, label.checkbox { 99 label.radio, label.checkbox {
100 font-weight: 400; 100 font-weight: 400;
101 + user-select: none;
101 input[type="radio"], input[type="checkbox"] { 102 input[type="radio"], input[type="checkbox"] {
102 margin-right: $-xs; 103 margin-right: $-xs;
103 } 104 }
104 } 105 }
105 106
107 +label.inline.checkbox {
108 + margin-right: $-m;
109 +}
110 +
106 label + p.small { 111 label + p.small {
107 margin-bottom: 0.8em; 112 margin-bottom: 0.8em;
108 } 113 }
109 114
110 -input[type="text"], input[type="number"], input[type="email"], input[type="search"], input[type="url"], input[type="password"], select, textarea { 115 +table.form-table {
116 + max-width: 100%;
117 + td {
118 + overflow: hidden;
119 + padding: $-xxs/2 0;
120 + }
121 +}
122 +
123 +input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
111 @extend .input-base; 124 @extend .input-base;
112 } 125 }
113 126
127 +input[type=date] {
128 + width: 190px;
129 +}
130 +
114 .toggle-switch { 131 .toggle-switch {
115 display: inline-block; 132 display: inline-block;
116 background-color: #BBB; 133 background-color: #BBB;
......
...@@ -109,6 +109,7 @@ ...@@ -109,6 +109,7 @@
109 transition-property: right, border; 109 transition-property: right, border;
110 border-left: 0px solid #FFF; 110 border-left: 0px solid #FFF;
111 background-color: #FFF; 111 background-color: #FFF;
112 + max-width: 320px;
112 &.fixed { 113 &.fixed {
113 background-color: #FFF; 114 background-color: #FFF;
114 z-index: 5; 115 z-index: 5;
......
...@@ -269,19 +269,31 @@ span.highlight { ...@@ -269,19 +269,31 @@ span.highlight {
269 /* 269 /*
270 * Lists 270 * Lists
271 */ 271 */
272 +ul, ol {
273 + overflow: hidden;
274 + p {
275 + margin: 0;
276 + }
277 +}
272 ul { 278 ul {
273 padding-left: $-m * 1.3; 279 padding-left: $-m * 1.3;
274 list-style: disc; 280 list-style: disc;
275 - overflow: hidden; 281 + ul {
282 + list-style: circle;
283 + margin-top: 0;
284 + margin-bottom: 0;
285 + }
286 + label {
287 + margin: 0;
288 + }
276 } 289 }
277 290
278 ol { 291 ol {
279 list-style: decimal; 292 list-style: decimal;
280 padding-left: $-m * 2; 293 padding-left: $-m * 2;
281 - overflow: hidden;
282 } 294 }
283 295
284 -li.checkbox-item { 296 +li.checkbox-item, li.task-list-item {
285 list-style: none; 297 list-style: none;
286 margin-left: - ($-m * 1.3); 298 margin-left: - ($-m * 1.3);
287 input[type="checkbox"] { 299 input[type="checkbox"] {
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
7 @import "grid"; 7 @import "grid";
8 @import "blocks"; 8 @import "blocks";
9 @import "buttons"; 9 @import "buttons";
10 -@import "forms";
11 @import "tables"; 10 @import "tables";
11 +@import "forms";
12 @import "animations"; 12 @import "animations";
13 @import "tinymce"; 13 @import "tinymce";
14 @import "highlightjs"; 14 @import "highlightjs";
...@@ -17,7 +17,11 @@ ...@@ -17,7 +17,11 @@
17 @import "lists"; 17 @import "lists";
18 @import "pages"; 18 @import "pages";
19 19
20 -[v-cloak], [v-show] {display: none;} 20 +[v-cloak], [v-show] {
21 + display: none; opacity: 0;
22 + animation-name: none !important;
23 +}
24 +
21 25
22 [ng\:cloak], [ng-cloak], .ng-cloak { 26 [ng\:cloak], [ng-cloak], .ng-cloak {
23 display: none !important; 27 display: none !important;
...@@ -272,8 +276,3 @@ $btt-size: 40px; ...@@ -272,8 +276,3 @@ $btt-size: 40px;
272 276
273 277
274 278
275 -
276 -
277 -
278 -
279 -
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Suchergebnisse', 45 'search_results' => 'Suchergebnisse',
46 - 'search_results_page' => 'Seiten-Suchergebnisse',
47 - 'search_results_chapter' => 'Kapitel-Suchergebnisse',
48 - 'search_results_book' => 'Buch-Suchergebnisse',
49 'search_clear' => 'Suche zur&uuml;cksetzen', 46 'search_clear' => 'Suche zur&uuml;cksetzen',
50 - 'search_view_pages' => 'Zeige alle passenden Seiten',
51 - 'search_view_chapters' => 'Zeige alle passenden Kapitel',
52 - 'search_view_books' => 'Zeige alle passenden B&uuml;cher',
53 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden', 47 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
54 'search_for_term' => 'Suche nach :term', 48 'search_for_term' => 'Suche nach :term',
55 - 'search_page_for_term' => 'Suche nach :term in Seiten',
56 - 'search_chapter_for_term' => 'Suche nach :term in Kapiteln',
57 - 'search_book_for_term' => 'Suche nach :term in B&uuml;chern',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -33,6 +33,7 @@ return [ ...@@ -33,6 +33,7 @@ return [
33 'search_clear' => 'Clear Search', 33 'search_clear' => 'Clear Search',
34 'reset' => 'Reset', 34 'reset' => 'Reset',
35 'remove' => 'Remove', 35 'remove' => 'Remove',
36 + 'add' => 'Add',
36 37
37 38
38 /** 39 /**
......
...@@ -14,6 +14,7 @@ return [ ...@@ -14,6 +14,7 @@ return [
14 'recent_activity' => 'Recent Activity', 14 'recent_activity' => 'Recent Activity',
15 'create_now' => 'Create one now', 15 'create_now' => 'Create one now',
16 'revisions' => 'Revisions', 16 'revisions' => 'Revisions',
17 + 'meta_revision' => 'Revision #:revisionCount',
17 'meta_created' => 'Created :timeLength', 18 'meta_created' => 'Created :timeLength',
18 'meta_created_name' => 'Created :timeLength by :user', 19 'meta_created_name' => 'Created :timeLength by :user',
19 'meta_updated' => 'Updated :timeLength', 20 'meta_updated' => 'Updated :timeLength',
...@@ -43,18 +44,26 @@ return [ ...@@ -43,18 +44,26 @@ return [
43 * Search 44 * Search
44 */ 45 */
45 'search_results' => 'Search Results', 46 'search_results' => 'Search Results',
46 - 'search_results_page' => 'Page Search Results', 47 + 'search_total_results_found' => ':count result found|:count total results found',
47 - 'search_results_chapter' => 'Chapter Search Results',
48 - 'search_results_book' => 'Book Search Results',
49 'search_clear' => 'Clear Search', 48 'search_clear' => 'Clear Search',
50 - 'search_view_pages' => 'View all matches pages',
51 - 'search_view_chapters' => 'View all matches chapters',
52 - 'search_view_books' => 'View all matches books',
53 'search_no_pages' => 'No pages matched this search', 49 'search_no_pages' => 'No pages matched this search',
54 'search_for_term' => 'Search for :term', 50 'search_for_term' => 'Search for :term',
55 - 'search_page_for_term' => 'Page search for :term', 51 + 'search_more' => 'More Results',
56 - 'search_chapter_for_term' => 'Chapter search for :term', 52 + 'search_filters' => 'Search Filters',
57 - 'search_book_for_term' => 'Books search for :term', 53 + 'search_content_type' => 'Content Type',
54 + 'search_exact_matches' => 'Exact Matches',
55 + 'search_tags' => 'Tag Searches',
56 + 'search_viewed_by_me' => 'Viewed by me',
57 + 'search_not_viewed_by_me' => 'Not viewed by me',
58 + 'search_permissions_set' => 'Permissions set',
59 + 'search_created_by_me' => 'Created by me',
60 + 'search_updated_by_me' => 'Updated by me',
61 + 'search_updated_before' => 'Updated before',
62 + 'search_updated_after' => 'Updated after',
63 + 'search_created_before' => 'Created before',
64 + 'search_created_after' => 'Created after',
65 + 'search_set_date' => 'Set Date',
66 + 'search_update' => 'Update Search',
58 67
59 /** 68 /**
60 * Books 69 * Books
...@@ -112,6 +121,7 @@ return [ ...@@ -112,6 +121,7 @@ return [
112 'chapters_empty' => 'No pages are currently in this chapter.', 121 'chapters_empty' => 'No pages are currently in this chapter.',
113 'chapters_permissions_active' => 'Chapter Permissions Active', 122 'chapters_permissions_active' => 'Chapter Permissions Active',
114 'chapters_permissions_success' => 'Chapter Permissions Updated', 123 'chapters_permissions_success' => 'Chapter Permissions Updated',
124 + 'chapters_search_this' => 'Search this chapter',
115 125
116 /** 126 /**
117 * Pages 127 * Pages
...@@ -159,6 +169,7 @@ return [ ...@@ -159,6 +169,7 @@ return [
159 'pages_revision_named' => 'Page Revision for :pageName', 169 'pages_revision_named' => 'Page Revision for :pageName',
160 'pages_revisions_created_by' => 'Created By', 170 'pages_revisions_created_by' => 'Created By',
161 'pages_revisions_date' => 'Revision Date', 171 'pages_revisions_date' => 'Revision Date',
172 + 'pages_revisions_number' => '#',
162 'pages_revisions_changelog' => 'Changelog', 173 'pages_revisions_changelog' => 'Changelog',
163 'pages_revisions_changes' => 'Changes', 174 'pages_revisions_changes' => 'Changes',
164 'pages_revisions_current' => 'Current Version', 175 'pages_revisions_current' => 'Current Version',
......
...@@ -120,6 +120,7 @@ return [ ...@@ -120,6 +120,7 @@ return [
120 'fr' => 'Français', 120 'fr' => 'Français',
121 'nl' => 'Nederlands', 121 'nl' => 'Nederlands',
122 'pt_BR' => 'Português do Brasil', 122 'pt_BR' => 'Português do Brasil',
123 + 'sk' => 'Slovensky',
123 ] 124 ]
124 /////////////////////////////////// 125 ///////////////////////////////////
125 ]; 126 ];
......
...@@ -11,7 +11,7 @@ return [ ...@@ -11,7 +11,7 @@ return [
11 | 11 |
12 */ 12 */
13 'failed' => 'Las credenciales no concuerdan con nuestros registros.', 13 'failed' => 'Las credenciales no concuerdan con nuestros registros.',
14 - 'throttle' => 'Demasiados intentos fallidos de conexiÃn. Por favor intente nuevamente en :seconds segundos.', 14 + 'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
15 15
16 /** 16 /**
17 * Login & Register 17 * Login & Register
......
...@@ -18,7 +18,7 @@ return [ ...@@ -18,7 +18,7 @@ return [
18 'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir', 18 'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
19 'images_deleted' => 'Imágenes borradas', 19 'images_deleted' => 'Imágenes borradas',
20 'image_preview' => 'Preview de la imagen', 20 'image_preview' => 'Preview de la imagen',
21 - 'image_upload_success' => 'Imagen subida exitosamente', 21 + 'image_upload_success' => 'Imagen subida éxitosamente',
22 'image_update_success' => 'Detalles de la imagen actualizados exitosamente', 22 'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
23 'image_delete_success' => 'Imagen borrada exitosamente' 23 'image_delete_success' => 'Imagen borrada exitosamente'
24 ]; 24 ];
......
...@@ -4,7 +4,7 @@ return [ ...@@ -4,7 +4,7 @@ return [
4 /** 4 /**
5 * Shared 5 * Shared
6 */ 6 */
7 - 'recently_created' => 'Recientemente creadod', 7 + 'recently_created' => 'Recientemente creado',
8 'recently_created_pages' => 'Páginas recientemente creadas', 8 'recently_created_pages' => 'Páginas recientemente creadas',
9 'recently_updated_pages' => 'Páginas recientemente actualizadas', 9 'recently_updated_pages' => 'Páginas recientemente actualizadas',
10 'recently_created_chapters' => 'Capítulos recientemente creados', 10 'recently_created_chapters' => 'Capítulos recientemente creados',
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Buscar resultados', 45 'search_results' => 'Buscar resultados',
46 - 'search_results_page' => 'resultados de búsqueda en página',
47 - 'search_results_chapter' => 'Resultados de búsqueda en capítulo ',
48 - 'search_results_book' => 'Resultados de búsqueda en libro',
49 'search_clear' => 'Limpiar resultados', 46 'search_clear' => 'Limpiar resultados',
50 - 'search_view_pages' => 'Ver todas las páginas que concuerdan',
51 - 'search_view_chapters' => 'Ver todos los capítulos que concuerdan',
52 - 'search_view_books' => 'Ver todos los libros que concuerdan',
53 'search_no_pages' => 'Ninguna página encontrada para la búsqueda', 47 'search_no_pages' => 'Ninguna página encontrada para la búsqueda',
54 'search_for_term' => 'Busqueda por :term', 48 'search_for_term' => 'Busqueda por :term',
55 - 'search_page_for_term' => 'Búsqueda de página por :term',
56 - 'search_chapter_for_term' => 'Búsqueda por capítulo de :term',
57 - 'search_book_for_term' => 'Búsqueda en libro de :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
...@@ -148,79 +139,79 @@ return [ ...@@ -148,79 +139,79 @@ return [
148 'pages_md_editor' => 'Editor', 139 'pages_md_editor' => 'Editor',
149 'pages_md_preview' => 'Preview', 140 'pages_md_preview' => 'Preview',
150 'pages_md_insert_image' => 'Insertar Imagen', 141 'pages_md_insert_image' => 'Insertar Imagen',
151 - 'pages_md_insert_link' => 'Insert Entity Link', 142 + 'pages_md_insert_link' => 'Insertar link de entidad',
152 - 'pages_not_in_chapter' => 'Page is not in a chapter', 143 + 'pages_not_in_chapter' => 'La página no esá en el caítulo',
153 - 'pages_move' => 'Move Page', 144 + 'pages_move' => 'Mover página',
154 - 'pages_move_success' => 'Page moved to ":parentName"', 145 + 'pages_move_success' => 'Página movida a ":parentName"',
155 - 'pages_permissions' => 'Page Permissions', 146 + 'pages_permissions' => 'Permisos de página',
156 - 'pages_permissions_success' => 'Page permissions updated', 147 + 'pages_permissions_success' => 'Permisos de página actualizados',
157 - 'pages_revisions' => 'Page Revisions', 148 + 'pages_revisions' => 'Revisiones de página',
158 - 'pages_revisions_named' => 'Page Revisions for :pageName', 149 + 'pages_revisions_named' => 'Revisiones de página para :pageName',
159 - 'pages_revision_named' => 'Page Revision for :pageName', 150 + 'pages_revision_named' => 'Revisión de ágina para :pageName',
160 - 'pages_revisions_created_by' => 'Created By', 151 + 'pages_revisions_created_by' => 'Creado por',
161 - 'pages_revisions_date' => 'Revision Date', 152 + 'pages_revisions_date' => 'Fecha de revisión',
162 'pages_revisions_changelog' => 'Changelog', 153 'pages_revisions_changelog' => 'Changelog',
163 - 'pages_revisions_changes' => 'Changes', 154 + 'pages_revisions_changes' => 'Cambios',
164 - 'pages_revisions_current' => 'Current Version', 155 + 'pages_revisions_current' => 'Versión actual',
165 'pages_revisions_preview' => 'Preview', 156 'pages_revisions_preview' => 'Preview',
166 - 'pages_revisions_restore' => 'Restore', 157 + 'pages_revisions_restore' => 'Restaurar',
167 - 'pages_revisions_none' => 'This page has no revisions', 158 + 'pages_revisions_none' => 'Esta página no tiene revisiones',
168 - 'pages_copy_link' => 'Copy Link', 159 + 'pages_copy_link' => 'Copiar Link',
169 - 'pages_permissions_active' => 'Page Permissions Active', 160 + 'pages_permissions_active' => 'Permisos de página activos',
170 - 'pages_initial_revision' => 'Initial publish', 161 + 'pages_initial_revision' => 'Publicación inicial',
171 - 'pages_initial_name' => 'New Page', 162 + 'pages_initial_name' => 'Página nueva',
172 - 'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.', 163 + 'pages_editing_draft_notification' => 'Ud. está actualmente editando un borrador que fue guardado porúltima vez el :timeDiff.',
173 - 'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.', 164 + 'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',
174 'pages_draft_edit_active' => [ 165 'pages_draft_edit_active' => [
175 - 'start_a' => ':count users have started editing this page', 166 + 'start_a' => ':count usuarios han comenzado a editar esta página',
176 - 'start_b' => ':userName has started editing this page', 167 + 'start_b' => ':userName ha comenzado a editar esta página',
177 - 'time_a' => 'since the pages was last updated', 168 + 'time_a' => 'desde que las página fue actualizada',
178 - 'time_b' => 'in the last :minCount minutes', 169 + 'time_b' => 'en los Ãltimos :minCount minutos',
179 - 'message' => ':start :time. Take care not to overwrite each other\'s updates!', 170 + 'message' => ':start :time. Ten cuidado de no sobreescribir los cambios del otro usuario',
180 ], 171 ],
181 - 'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content', 172 + 'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
182 173
183 /** 174 /**
184 * Editor sidebar 175 * Editor sidebar
185 */ 176 */
186 - 'page_tags' => 'Page Tags', 177 + 'page_tags' => 'Etiquetas de página',
187 - 'tag' => 'Tag', 178 + 'tag' => 'Etiqueta',
188 - 'tags' => '', 179 + 'tags' => 'Etiquetas',
189 - 'tag_value' => 'Tag Value (Optional)', 180 + 'tag_value' => 'Valor de la etiqueta (Opcional)',
190 - 'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.", 181 + 'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Ud. puede asignar un valor a una etiqueta para una organizacón a mayor detalle.",
191 - 'tags_add' => 'Add another tag', 182 + 'tags_add' => 'Agregar otra etiqueta',
192 - 'attachments' => 'Attachments', 183 + 'attachments' => 'Adjuntos',
193 - 'attachments_explain' => 'Upload some files or attach some link to display on your page. These are visible in the page sidebar.', 184 + 'attachments_explain' => 'Subir ficheros o agregar links para mostrar en la página. Estos son visibles en la barra lateral de la página.',
194 - 'attachments_explain_instant_save' => 'Changes here are saved instantly.', 185 + 'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
195 - 'attachments_items' => 'Attached Items', 186 + 'attachments_items' => 'Items adjuntados',
196 - 'attachments_upload' => 'Upload File', 187 + 'attachments_upload' => 'Fichero adjuntado',
197 - 'attachments_link' => 'Attach Link', 188 + 'attachments_link' => 'Adjuntar Link',
198 - 'attachments_set_link' => 'Set Link', 189 + 'attachments_set_link' => 'Setear Link',
199 - 'attachments_delete_confirm' => 'Click delete again to confirm you want to delete this attachment.', 190 + 'attachments_delete_confirm' => 'Haga click en borrar nuevamente para confirmar que quiere borrar este adjunto.',
200 - 'attachments_dropzone' => 'Drop files or click here to attach a file', 191 + 'attachments_dropzone' => 'Arrastre ficheros aquío haga click aquípara adjuntar un fichero',
201 - 'attachments_no_files' => 'No files have been uploaded', 192 + 'attachments_no_files' => 'NingÃn fichero ha sido adjuntado',
202 - 'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.', 193 + 'attachments_explain_link' => 'Ud. puede agregar un link o si lo prefiere puede agregar un fichero. Esto puede ser un link a otra página o un link a un fichero en la nube.',
203 - 'attachments_link_name' => 'Link Name', 194 + 'attachments_link_name' => 'Nombre de Link',
204 - 'attachment_link' => 'Attachment link', 195 + 'attachment_link' => 'Link adjunto',
205 - 'attachments_link_url' => 'Link to file', 196 + 'attachments_link_url' => 'Link a fichero',
206 - 'attachments_link_url_hint' => 'Url of site or file', 197 + 'attachments_link_url_hint' => 'Url del sitio o fichero',
207 - 'attach' => 'Attach', 198 + 'attach' => 'Adjuntar',
208 - 'attachments_edit_file' => 'Edit File', 199 + 'attachments_edit_file' => 'Editar fichero',
209 - 'attachments_edit_file_name' => 'File Name', 200 + 'attachments_edit_file_name' => 'Nombre del fichero',
210 - 'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite', 201 + 'attachments_edit_drop_upload' => 'Arrastre a los ficheros o haga click aquípara subir o sobreescribir',
211 - 'attachments_order_updated' => 'Attachment order updated', 202 + 'attachments_order_updated' => 'Orden de adjuntos actualizado',
212 - 'attachments_updated_success' => 'Attachment details updated', 203 + 'attachments_updated_success' => 'Detalles de adjuntos actualizados',
213 - 'attachments_deleted' => 'Attachment deleted', 204 + 'attachments_deleted' => 'Adjunto borrado',
214 - 'attachments_file_uploaded' => 'File successfully uploaded', 205 + 'attachments_file_uploaded' => 'Fichero subido éxitosamente',
215 - 'attachments_file_updated' => 'File successfully updated', 206 + 'attachments_file_updated' => 'Fichero actualizado éxitosamente',
216 - 'attachments_link_attached' => 'Link successfully attached to page', 207 + 'attachments_link_attached' => 'Link agregado éxitosamente a la ágina',
217 208
218 /** 209 /**
219 * Profile View 210 * Profile View
220 */ 211 */
221 - 'profile_user_for_x' => 'User for :time', 212 + 'profile_user_for_x' => 'Usuario para :time',
222 - 'profile_created_content' => 'Created Content', 213 + 'profile_created_content' => 'Contenido creado',
223 - 'profile_not_created_pages' => ':userName has not created any pages', 214 + 'profile_not_created_pages' => ':userName no ha creado ninguna página',
224 - 'profile_not_created_chapters' => ':userName has not created any chapters', 215 + 'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
225 - 'profile_not_created_books' => ':userName has not created any books', 216 + 'profile_not_created_books' => ':userName no ha creado ningún libro',
226 ]; 217 ];
......
...@@ -7,64 +7,64 @@ return [ ...@@ -7,64 +7,64 @@ return [
7 */ 7 */
8 8
9 // Permissions 9 // Permissions
10 - 'permission' => 'You do not have permission to access the requested page.', 10 + 'permission' => 'Ud. no tiene permisos para visualizar la página solicitada.',
11 - 'permissionJson' => 'You do not have permission to perform the requested action.', 11 + 'permissionJson' => 'Ud. no tiene permisos para ejecutar la acción solicitada.',
12 12
13 // Auth 13 // Auth
14 - 'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.', 14 + 'error_user_exists_different_creds' => 'Un usuario con el email :email ya existe pero con credenciales diferentes.',
15 - 'email_already_confirmed' => 'Email has already been confirmed, Try logging in.', 15 + 'email_already_confirmed' => 'El email ya ha sido confirmado, Intente loguearse en la aplicación.',
16 - 'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.', 16 + 'email_confirmation_invalid' => 'Este token de confirmación no e válido o ya ha sido usado,Intente registrar uno nuevamente.',
17 - 'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.', 17 + 'email_confirmation_expired' => 'El token de confirmación ha expirado, Un nuevo email de confirmacón ha sido enviado.',
18 - 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', 18 + 'ldap_fail_anonymous' => 'El acceso con LDAP ha fallado usando binding anónimo',
19 - 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', 19 + 'ldap_fail_authed' => 'El acceso LDAP usando el dn & password detallados',
20 - 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', 20 + 'ldap_extension_not_installed' => 'La extensión LDAP PHP no se encuentra instalada',
21 - 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', 21 + 'ldap_cannot_connect' => 'No se puede conectar con el servidor ldap, la conexión inicial ha fallado',
22 - 'social_no_action_defined' => 'No action defined', 22 + 'social_no_action_defined' => 'Acción no definida',
23 - 'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.', 23 + 'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente loguearse a través de la opcón :socialAccount .',
24 - 'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.', 24 + 'social_account_email_in_use' => 'El email :email ya se encuentra en uso. Si ud. ya dispone de una cuenta puede loguearse a través de su cuenta :socialAccount desde la configuración de perfil.',
25 - 'social_account_existing' => 'This :socialAccount is already attached to your profile.', 25 + 'social_account_existing' => 'La cuenta :socialAccount ya se encuentra asignada a su perfil.',
26 - 'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.', 26 + 'social_account_already_used_existing' => 'La cuenta :socialAccount ya se encuentra usada por otro usuario.',
27 - 'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ', 27 + 'social_account_not_used' => 'La cuenta :socialAccount no está asociada a ningún usuario. Por favor adjuntela a su configuración de perfil. ',
28 - 'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.', 28 + 'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
29 - 'social_driver_not_found' => 'Social driver not found', 29 + 'social_driver_not_found' => 'Driver social no encontrado',
30 - 'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.', 30 + 'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
31 31
32 // System 32 // System
33 - 'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.', 33 + 'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',
34 - 'cannot_get_image_from_url' => 'Cannot get image from :url', 34 + 'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',
35 - 'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.', 35 + 'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',
36 - 'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.', 36 + 'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',
37 - 'image_upload_error' => 'An error occurred uploading the image', 37 + 'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
38 38
39 // Attachments 39 // Attachments
40 - 'attachment_page_mismatch' => 'Page mismatch during attachment update', 40 + 'attachment_page_mismatch' => 'Página no coincidente durante la subida del adjunto ',
41 41
42 // Pages 42 // Pages
43 - 'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page', 43 + 'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegurese de que tiene conexión a Internet antes de guardar este borrador',
44 44
45 // Entities 45 // Entities
46 - 'entity_not_found' => 'Entity not found', 46 + 'entity_not_found' => 'Entidad no encontrada',
47 - 'book_not_found' => 'Book not found', 47 + 'book_not_found' => 'Libro no encontrado',
48 - 'page_not_found' => 'Page not found', 48 + 'page_not_found' => 'Página no encontrada',
49 - 'chapter_not_found' => 'Chapter not found', 49 + 'chapter_not_found' => 'Capítulo no encontrado',
50 - 'selected_book_not_found' => 'The selected book was not found', 50 + 'selected_book_not_found' => 'El libro seleccionado no fue encontrado',
51 - 'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found', 51 + 'selected_book_chapter_not_found' => 'El libro o capítulo seleccionado no fue encontrado',
52 - 'guests_cannot_save_drafts' => 'Guests cannot save drafts', 52 + 'guests_cannot_save_drafts' => 'Los invitados no pueden guardar los borradores',
53 53
54 // Users 54 // Users
55 - 'users_cannot_delete_only_admin' => 'You cannot delete the only admin', 55 + 'users_cannot_delete_only_admin' => 'No se puede borrar el único administrador',
56 - 'users_cannot_delete_guest' => 'You cannot delete the guest user', 56 + 'users_cannot_delete_guest' => 'No se puede borrar el usuario invitado',
57 57
58 // Roles 58 // Roles
59 - 'role_cannot_be_edited' => 'This role cannot be edited', 59 + 'role_cannot_be_edited' => 'Este rol no puede ser editado',
60 - 'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted', 60 + 'role_system_cannot_be_deleted' => 'Este rol es un rol de sistema y no puede ser borrado',
61 - 'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role', 61 + 'role_registration_default_cannot_delete' => 'Este rol no puede ser borrado mientras sea el rol por defecto de registro',
62 62
63 // Error pages 63 // Error pages
64 - '404_page_not_found' => 'Page Not Found', 64 + '404_page_not_found' => 'Página no encontrada',
65 - 'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.', 65 + 'sorry_page_not_found' => 'Lo sentimos, la página que intenta acceder no pudo ser encontrada.',
66 - 'return_home' => 'Return to home', 66 + 'return_home' => 'Volver al home',
67 - 'error_occurred' => 'An Error Occurred', 67 + 'error_occurred' => 'Ha ocurrido un error',
68 - 'app_down' => ':appName is down right now', 68 + 'app_down' => 'La aplicación :appName se encuentra caída en este momento',
69 - 'back_soon' => 'It will be back up soon.', 69 + 'back_soon' => 'Volverá a estar operativa en corto tiempo.',
70 ]; 70 ];
......
...@@ -8,105 +8,105 @@ return [ ...@@ -8,105 +8,105 @@ return [
8 * including users and roles. 8 * including users and roles.
9 */ 9 */
10 10
11 - 'settings' => 'Settings', 11 + 'settings' => 'Ajustes',
12 - 'settings_save' => 'Save Settings', 12 + 'settings_save' => 'Guardar ajustes',
13 - 'settings_save_success' => 'Settings saved', 13 + 'settings_save_success' => 'Ajustes guardados',
14 14
15 /** 15 /**
16 * App settings 16 * App settings
17 */ 17 */
18 18
19 - 'app_settings' => 'App Settings', 19 + 'app_settings' => 'Ajustes de App',
20 - 'app_name' => 'Application name', 20 + 'app_name' => 'Nombre de aplicación',
21 - 'app_name_desc' => 'This name is shown in the header and any emails.', 21 + 'app_name_desc' => 'Este nombre es mostrado en la cabecera y en cualquier email de la aplicación',
22 - 'app_name_header' => 'Show Application name in header?', 22 + 'app_name_header' => 'Mostrar el nombre de la aplicación en la cabecera?',
23 - 'app_public_viewing' => 'Allow public viewing?', 23 + 'app_public_viewing' => 'Permitir vista pública?',
24 - 'app_secure_images' => 'Enable higher security image uploads?', 24 + 'app_secure_images' => 'Habilitar mayor seguridad para subir imágenes?',
25 - 'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.', 25 + 'app_secure_images_desc' => 'Por razones de performance, todas las imágenes son púicas. Esta opción agrega una cadena larga difícil de adivinar, asegúrese que los indices de directorios no esán habilitados para prevenir el acceso fácil a las imágenes.',
26 - 'app_editor' => 'Page editor', 26 + 'app_editor' => 'Editor de página',
27 - 'app_editor_desc' => 'Select which editor will be used by all users to edit pages.', 27 + 'app_editor_desc' => 'Seleccione cuál editor ser usado por todos los usuarios para editar páginas.',
28 - 'app_custom_html' => 'Custom HTML head content', 28 + 'app_custom_html' => 'Contenido de cabecera HTML customizable',
29 - 'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.', 29 + 'app_custom_html_desc' => 'Cualquier contenido agregado aquíseráinsertado al final de la secón <head> de cada ágina. Esto esútil para sobreescribir estilo o agregar código para anaíticas.',
30 - 'app_logo' => 'Application logo', 30 + 'app_logo' => 'Logo de la aplicación',
31 - 'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.', 31 + 'app_logo_desc' => 'Esta imagen debería de ser 43px en altura. <br>Iágenes grandes seán escaladas.',
32 - 'app_primary_color' => 'Application primary color', 32 + 'app_primary_color' => 'Color primario de la aplicación',
33 - 'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.', 33 + 'app_primary_color_desc' => 'Esto debería ser un valor hexadecimal. <br>Deje el valor vaío para reiniciar al valor por defecto.',
34 34
35 /** 35 /**
36 * Registration settings 36 * Registration settings
37 */ 37 */
38 38
39 - 'reg_settings' => 'Registration Settings', 39 + 'reg_settings' => 'Ajustes de registro',
40 - 'reg_allow' => 'Allow registration?', 40 + 'reg_allow' => 'Permitir registro?',
41 - 'reg_default_role' => 'Default user role after registration', 41 + 'reg_default_role' => 'Rol de usuario por defecto despúes del registro',
42 - 'reg_confirm_email' => 'Require email confirmation?', 42 + 'reg_confirm_email' => 'Requerir email de confirmaación?',
43 - 'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.', 43 + 'reg_confirm_email_desc' => 'Si la restricción por dominio es usada, entonces la confirmaciónpor email serárequerida y el valor a continuón será ignorado.',
44 - 'reg_confirm_restrict_domain' => 'Restrict registration to domain', 44 + 'reg_confirm_restrict_domain' => 'Restringir registro al dominio',
45 - 'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.', 45 + 'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comasa de los emails del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les seá enviado un emal para confirmar la dirección antes de que se le permita interactuar con la aplicacón. <br> Note que los usuarios se les permitir ácambiar sus direcciones de email luego de un registr éxioso.',
46 - 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', 46 + 'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
47 47
48 /** 48 /**
49 * Role settings 49 * Role settings
50 */ 50 */
51 51
52 'roles' => 'Roles', 52 'roles' => 'Roles',
53 - 'role_user_roles' => 'User Roles', 53 + 'role_user_roles' => 'Roles de usuario',
54 - 'role_create' => 'Create New Role', 54 + 'role_create' => 'Crear nuevo rol',
55 - 'role_create_success' => 'Role successfully created', 55 + 'role_create_success' => 'Rol creado satisfactoriamente',
56 - 'role_delete' => 'Delete Role', 56 + 'role_delete' => 'Borrar rol',
57 - 'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.', 57 + 'role_delete_confirm' => 'Se borrará el rol con nombre \':roleName\'.',
58 - 'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.', 58 + 'role_delete_users_assigned' => 'Este rol tiene :userCount usuarios asignados. Si ud. quisiera migrar los usuarios de este rol, seleccione un nuevo rol a continuación.',
59 - 'role_delete_no_migration' => "Don't migrate users", 59 + 'role_delete_no_migration' => "No migrar usuarios",
60 - 'role_delete_sure' => 'Are you sure you want to delete this role?', 60 + 'role_delete_sure' => 'Está seguro que desea borrar este rol?',
61 - 'role_delete_success' => 'Role successfully deleted', 61 + 'role_delete_success' => 'Rol borrado satisfactoriamente',
62 - 'role_edit' => 'Edit Role', 62 + 'role_edit' => 'Editar rol',
63 - 'role_details' => 'Role Details', 63 + 'role_details' => 'Detalles de rol',
64 - 'role_name' => 'Role Name', 64 + 'role_name' => 'Nombre de rol',
65 - 'role_desc' => 'Short Description of Role', 65 + 'role_desc' => 'Descripción corta de rol',
66 - 'role_system' => 'System Permissions', 66 + 'role_system' => 'Permisos de sistema',
67 - 'role_manage_users' => 'Manage users', 67 + 'role_manage_users' => 'Gestionar usuarios',
68 - 'role_manage_roles' => 'Manage roles & role permissions', 68 + 'role_manage_roles' => 'Gestionar roles y permisos de roles',
69 - 'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions', 69 + 'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y áginas',
70 - 'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages', 70 + 'role_manage_own_entity_permissions' => 'Gestionar permisos en libros propios, capítulos y páginas',
71 - 'role_manage_settings' => 'Manage app settings', 71 + 'role_manage_settings' => 'Gestionar ajustes de activos',
72 - 'role_asset' => 'Asset Permissions', 72 + 'role_asset' => 'Permisos de activos',
73 - 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', 73 + 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos a Libros, Capítulos y áginas sobreescribiran estos permisos.',
74 - 'role_all' => 'All', 74 + 'role_all' => 'Todo',
75 - 'role_own' => 'Own', 75 + 'role_own' => 'Propio',
76 - 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', 76 + 'role_controlled_by_asset' => 'Controlado por el actvo al que ha sido subido',
77 - 'role_save' => 'Save Role', 77 + 'role_save' => 'Guardar rol',
78 - 'role_update_success' => 'Role successfully updated', 78 + 'role_update_success' => 'Rol actualizado éxitosamente',
79 - 'role_users' => 'Users in this role', 79 + 'role_users' => 'Usuarios en este rol',
80 - 'role_users_none' => 'No users are currently assigned to this role', 80 + 'role_users_none' => 'No hay usuarios asignados a este rol',
81 81
82 /** 82 /**
83 * Users 83 * Users
84 */ 84 */
85 85
86 - 'users' => 'Users', 86 + 'users' => 'Usuarios',
87 - 'user_profile' => 'User Profile', 87 + 'user_profile' => 'Perfil de usuario',
88 - 'users_add_new' => 'Add New User', 88 + 'users_add_new' => 'Agregar nuevo usuario',
89 - 'users_search' => 'Search Users', 89 + 'users_search' => 'Buscar usuarios',
90 - 'users_role' => 'User Roles', 90 + 'users_role' => 'Roles de usuario',
91 - 'users_external_auth_id' => 'External Authentication ID', 91 + 'users_external_auth_id' => 'ID externo de autenticación',
92 - 'users_password_warning' => 'Only fill the below if you would like to change your password:', 92 + 'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
93 - 'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.', 93 + 'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sio que es asignado automáticamente.',
94 - 'users_delete' => 'Delete User', 94 + 'users_delete' => 'Borrar usuario',
95 - 'users_delete_named' => 'Delete user :userName', 95 + 'users_delete_named' => 'Borrar usuario :userName',
96 - 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', 96 + 'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',
97 - 'users_delete_confirm' => 'Are you sure you want to delete this user?', 97 + 'users_delete_confirm' => 'Está seguro que desea borrar este usuario?',
98 - 'users_delete_success' => 'Users successfully removed', 98 + 'users_delete_success' => 'Usuarios removidos éxitosamente',
99 - 'users_edit' => 'Edit User', 99 + 'users_edit' => 'Editar Usuario',
100 - 'users_edit_profile' => 'Edit Profile', 100 + 'users_edit_profile' => 'Editar perfil',
101 - 'users_edit_success' => 'User successfully updated', 101 + 'users_edit_success' => 'Usuario actualizado',
102 - 'users_avatar' => 'User Avatar', 102 + 'users_avatar' => 'Avatar del usuario',
103 - 'users_avatar_desc' => 'This image should be approx 256px square.', 103 + 'users_avatar_desc' => 'Esta imagen debe ser aproximadamente 256px por lado.',
104 - 'users_preferred_language' => 'Preferred Language', 104 + 'users_preferred_language' => 'Lenguaje preferido',
105 - 'users_social_accounts' => 'Social Accounts', 105 + 'users_social_accounts' => 'Cuentas sociales',
106 - 'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.', 106 + 'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un ápido y ás ácil login. Desconectando una cuenta aqu íno revca accesos ya autorizados. Revoque el acceso desde se perfil desde los ajustes de perfil en la cuenta social conectada.',
107 - 'users_social_connect' => 'Connect Account', 107 + 'users_social_connect' => 'Conectar cuenta',
108 - 'users_social_disconnect' => 'Disconnect Account', 108 + 'users_social_disconnect' => 'Desconectar cuenta',
109 - 'users_social_connected' => ':socialAccount account was successfully attached to your profile.', 109 + 'users_social_connected' => 'La cuenta :socialAccount ha sido éxitosamente añadida a su perfil.',
110 - 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', 110 + 'users_social_disconnected' => 'La cuenta :socialAccount ha sido desconectada éxitosamente de su perfil.',
111 111
112 ]; 112 ];
......
...@@ -13,67 +13,67 @@ return [ ...@@ -13,67 +13,67 @@ return [
13 | 13 |
14 */ 14 */
15 15
16 - 'accepted' => 'The :attribute must be accepted.', 16 + 'accepted' => 'El :attribute debe ser aceptado.',
17 - 'active_url' => 'The :attribute is not a valid URL.', 17 + 'active_url' => 'El :attribute no es una URl válida.',
18 - 'after' => 'The :attribute must be a date after :date.', 18 + 'after' => 'El :attribute debe ser una fecha posterior :date.',
19 - 'alpha' => 'The :attribute may only contain letters.', 19 + 'alpha' => 'El :attribute solo puede contener letras.',
20 - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 20 + 'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.',
21 - 'alpha_num' => 'The :attribute may only contain letters and numbers.', 21 + 'alpha_num' => 'El :attribute solo puede contener letras y número.',
22 - 'array' => 'The :attribute must be an array.', 22 + 'array' => 'El :attribute debe de ser un array.',
23 - 'before' => 'The :attribute must be a date before :date.', 23 + 'before' => 'El :attribute debe ser una fecha anterior a :date.',
24 'between' => [ 24 'between' => [
25 - 'numeric' => 'The :attribute must be between :min and :max.', 25 + 'numeric' => 'El :attribute debe estar entre :min y :max.',
26 - 'file' => 'The :attribute must be between :min and :max kilobytes.', 26 + 'file' => 'El :attribute debe estar entre :min y :max kilobytes.',
27 - 'string' => 'The :attribute must be between :min and :max characters.', 27 + 'string' => 'El :attribute debe estar entre :min y :max carácteres.',
28 - 'array' => 'The :attribute must have between :min and :max items.', 28 + 'array' => 'El :attribute debe estar entre :min y :max items.',
29 ], 29 ],
30 - 'boolean' => 'The :attribute field must be true or false.', 30 + 'boolean' => 'El campo :attribute debe ser true o false.',
31 - 'confirmed' => 'The :attribute confirmation does not match.', 31 + 'confirmed' => 'La confirmación de :attribute no concuerda.',
32 - 'date' => 'The :attribute is not a valid date.', 32 + 'date' => 'El :attribute no es una fecha válida.',
33 - 'date_format' => 'The :attribute does not match the format :format.', 33 + 'date_format' => 'El :attribute no coincide con el formato :format.',
34 - 'different' => 'The :attribute and :other must be different.', 34 + 'different' => ':attribute y :other deben ser diferentes.',
35 - 'digits' => 'The :attribute must be :digits digits.', 35 + 'digits' => ':attribute debe ser de :digits dígitos.',
36 - 'digits_between' => 'The :attribute must be between :min and :max digits.', 36 + 'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
37 - 'email' => 'The :attribute must be a valid email address.', 37 + 'email' => ':attribute debe ser una dirección álida.',
38 - 'filled' => 'The :attribute field is required.', 38 + 'filled' => 'El campo :attribute es requerido.',
39 - 'exists' => 'The selected :attribute is invalid.', 39 + 'exists' => 'El :attribute seleccionado es inválido.',
40 - 'image' => 'The :attribute must be an image.', 40 + 'image' => 'El :attribute debe ser una imagen.',
41 - 'in' => 'The selected :attribute is invalid.', 41 + 'in' => 'El selected :attribute es inválio.',
42 - 'integer' => 'The :attribute must be an integer.', 42 + 'integer' => 'El :attribute debe ser un entero.',
43 - 'ip' => 'The :attribute must be a valid IP address.', 43 + 'ip' => 'El :attribute debe ser una dirección IP álida.',
44 'max' => [ 44 'max' => [
45 - 'numeric' => 'The :attribute may not be greater than :max.', 45 + 'numeric' => ':attribute no puede ser mayor que :max.',
46 - 'file' => 'The :attribute may not be greater than :max kilobytes.', 46 + 'file' => ':attribute no puede ser mayor que :max kilobytes.',
47 - 'string' => 'The :attribute may not be greater than :max characters.', 47 + 'string' => ':attribute no puede ser mayor que :max carácteres.',
48 - 'array' => 'The :attribute may not have more than :max items.', 48 + 'array' => ':attribute no puede contener más de :max items.',
49 ], 49 ],
50 - 'mimes' => 'The :attribute must be a file of type: :values.', 50 + 'mimes' => ':attribute debe ser un fichero de tipo: :values.',
51 'min' => [ 51 'min' => [
52 - 'numeric' => 'The :attribute must be at least :min.', 52 + 'numeric' => ':attribute debe ser al menos de :min.',
53 - 'file' => 'The :attribute must be at least :min kilobytes.', 53 + 'file' => ':attribute debe ser al menos :min kilobytes.',
54 - 'string' => 'The :attribute must be at least :min characters.', 54 + 'string' => ':attribute debe ser al menos :min caracteres.',
55 - 'array' => 'The :attribute must have at least :min items.', 55 + 'array' => ':attribute debe tener como mínimo :min items.',
56 ], 56 ],
57 - 'not_in' => 'The selected :attribute is invalid.', 57 + 'not_in' => ':attribute seleccionado es inválio.',
58 - 'numeric' => 'The :attribute must be a number.', 58 + 'numeric' => ':attribute debe ser numérico.',
59 - 'regex' => 'The :attribute format is invalid.', 59 + 'regex' => ':attribute con formato inválido',
60 - 'required' => 'The :attribute field is required.', 60 + 'required' => ':attribute es requerido.',
61 - 'required_if' => 'The :attribute field is required when :other is :value.', 61 + 'required_if' => ':attribute es requerido cuando :other vale :value.',
62 - 'required_with' => 'The :attribute field is required when :values is present.', 62 + 'required_with' => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',
63 - 'required_with_all' => 'The :attribute field is required when :values is present.', 63 + 'required_with_all' => 'El campo :attribute es requerido cuando los valores sean :values.',
64 - 'required_without' => 'The :attribute field is required when :values is not present.', 64 + 'required_without' => ':attribute es requerido cuando no se encuentre entre los valores :values.',
65 - 'required_without_all' => 'The :attribute field is required when none of :values are present.', 65 + 'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',
66 - 'same' => 'The :attribute and :other must match.', 66 + 'same' => ':attribute y :other deben coincidir.',
67 'size' => [ 67 'size' => [
68 - 'numeric' => 'The :attribute must be :size.', 68 + 'numeric' => ':attribute debe ser :size.',
69 - 'file' => 'The :attribute must be :size kilobytes.', 69 + 'file' => ':attribute debe ser :size kilobytes.',
70 - 'string' => 'The :attribute must be :size characters.', 70 + 'string' => ':attribute debe ser :size caracteres.',
71 - 'array' => 'The :attribute must contain :size items.', 71 + 'array' => ':attribute debe contener :size items.',
72 ], 72 ],
73 - 'string' => 'The :attribute must be a string.', 73 + 'string' => 'El atributo :attribute debe ser una cadena.',
74 - 'timezone' => 'The :attribute must be a valid zone.', 74 + 'timezone' => 'El atributo :attribute debe ser una zona válida.',
75 - 'unique' => 'The :attribute has already been taken.', 75 + 'unique' => 'El atributo :attribute ya ha sido tomado.',
76 - 'url' => 'The :attribute format is invalid.', 76 + 'url' => 'El atributo :attribute tiene un formato inválid.',
77 77
78 /* 78 /*
79 |-------------------------------------------------------------------------- 79 |--------------------------------------------------------------------------
...@@ -88,7 +88,7 @@ return [ ...@@ -88,7 +88,7 @@ return [
88 88
89 'custom' => [ 89 'custom' => [
90 'password-confirm' => [ 90 'password-confirm' => [
91 - 'required_with' => 'Password confirmation required', 91 + 'required_with' => 'Confirmación de Password requerida',
92 ], 92 ],
93 ], 93 ],
94 94
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Résultats de recherche', 45 'search_results' => 'Résultats de recherche',
46 - 'search_results_page' => 'Résultats de recherche des pages',
47 - 'search_results_chapter' => 'Résultats de recherche des chapitres',
48 - 'search_results_book' => 'Résultats de recherche des livres',
49 'search_clear' => 'Réinitialiser la recherche', 46 'search_clear' => 'Réinitialiser la recherche',
50 - 'search_view_pages' => 'Voir toutes les pages correspondantes',
51 - 'search_view_chapters' => 'Voir tous les chapitres correspondants',
52 - 'search_view_books' => 'Voir tous les livres correspondants',
53 'search_no_pages' => 'Aucune page correspondant à cette recherche', 47 'search_no_pages' => 'Aucune page correspondant à cette recherche',
54 'search_for_term' => 'recherche pour :term', 48 'search_for_term' => 'recherche pour :term',
55 - 'search_page_for_term' => 'Recherche de page pour :term',
56 - 'search_chapter_for_term' => 'Recherche de chapitre pour :term',
57 - 'search_book_for_term' => 'Recherche de livres pour :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Zoekresultaten', 45 'search_results' => 'Zoekresultaten',
46 - 'search_results_page' => 'Pagina Zoekresultaten',
47 - 'search_results_chapter' => 'Hoofdstuk Zoekresultaten',
48 - 'search_results_book' => 'Boek Zoekresultaten',
49 'search_clear' => 'Zoekopdracht wissen', 46 'search_clear' => 'Zoekopdracht wissen',
50 - 'search_view_pages' => 'Bekijk alle gevonden pagina\'s',
51 - 'search_view_chapters' => 'Bekijk alle gevonden hoofdstukken',
52 - 'search_view_books' => 'Bekijk alle gevonden boeken',
53 'search_no_pages' => 'Er zijn geen pagina\'s gevonden', 47 'search_no_pages' => 'Er zijn geen pagina\'s gevonden',
54 'search_for_term' => 'Zoeken op :term', 48 'search_for_term' => 'Zoeken op :term',
55 - 'search_page_for_term' => 'Pagina doorzoeken op :term',
56 - 'search_chapter_for_term' => 'Hoofdstuk doorzoeken op :term',
57 - 'search_book_for_term' => 'Boeken doorzoeken op :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
...@@ -43,18 +43,9 @@ return [ ...@@ -43,18 +43,9 @@ return [
43 * Search 43 * Search
44 */ 44 */
45 'search_results' => 'Resultado(s) da Pesquisa', 45 'search_results' => 'Resultado(s) da Pesquisa',
46 - 'search_results_page' => 'Resultado(s) de Pesquisa de Página',
47 - 'search_results_chapter' => 'Resultado(s) de Pesquisa de Capítulo',
48 - 'search_results_book' => 'Resultado(s) de Pesquisa de Livro',
49 'search_clear' => 'Limpar Pesquisa', 46 'search_clear' => 'Limpar Pesquisa',
50 - 'search_view_pages' => 'Visualizar todas as páginas correspondentes',
51 - 'search_view_chapters' => 'Visualizar todos os capítulos correspondentes',
52 - 'search_view_books' => 'Visualizar todos os livros correspondentes',
53 'search_no_pages' => 'Nenhuma página corresponde à pesquisa', 47 'search_no_pages' => 'Nenhuma página corresponde à pesquisa',
54 'search_for_term' => 'Pesquisar por :term', 48 'search_for_term' => 'Pesquisar por :term',
55 - 'search_page_for_term' => 'Pesquisar Página por :term',
56 - 'search_chapter_for_term' => 'Pesquisar Capítulo por :term',
57 - 'search_book_for_term' => 'Pesquisar Livros por :term',
58 49
59 /** 50 /**
60 * Books 51 * Books
......
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Activity text strings.
7 + * Is used for all the text within activity logs & notifications.
8 + */
9 +
10 + // Pages
11 + 'page_create' => 'vytvoril stránku',
12 + 'page_create_notification' => 'Stránka úspešne vytvorená',
13 + 'page_update' => 'aktualizoval stránku',
14 + 'page_update_notification' => 'Stránka úspešne aktualizovaná',
15 + 'page_delete' => 'odstránil stránku',
16 + 'page_delete_notification' => 'Stránka úspešne odstránená',
17 + 'page_restore' => 'obnovil stránku',
18 + 'page_restore_notification' => 'Stránka úspešne obnovená',
19 + 'page_move' => 'presunul stránku',
20 +
21 + // Chapters
22 + 'chapter_create' => 'vytvoril kapitolu',
23 + 'chapter_create_notification' => 'Kapitola úspešne vytvorená',
24 + 'chapter_update' => 'aktualizoval kapitolu',
25 + 'chapter_update_notification' => 'Kapitola úspešne aktualizovaná',
26 + 'chapter_delete' => 'odstránil kapitolu',
27 + 'chapter_delete_notification' => 'Kapitola úspešne odstránená',
28 + 'chapter_move' => 'presunul kapitolu',
29 +
30 + // Books
31 + 'book_create' => 'vytvoril knihu',
32 + 'book_create_notification' => 'Kniha úspešne vytvorená',
33 + 'book_update' => 'aktualizoval knihu',
34 + 'book_update_notification' => 'Kniha úspešne aktualizovaná',
35 + 'book_delete' => 'odstránil knihu',
36 + 'book_delete_notification' => 'Kniha úspešne odstránená',
37 + 'book_sort' => 'zoradil knihu',
38 + 'book_sort_notification' => 'Kniha úspešne znovu zoradená',
39 +
40 +];
1 +<?php
2 +return [
3 + /*
4 + |--------------------------------------------------------------------------
5 + | Authentication Language Lines
6 + |--------------------------------------------------------------------------
7 + |
8 + | The following language lines are used during authentication for various
9 + | messages that we need to display to the user. You are free to modify
10 + | these language lines according to your application's requirements.
11 + |
12 + */
13 + 'failed' => 'Tieto údaje nesedia s našimi záznamami.',
14 + 'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.',
15 +
16 + /**
17 + * Login & Register
18 + */
19 + 'sign_up' => 'Registrácia',
20 + 'log_in' => 'Prihlásenie',
21 + 'log_in_with' => 'Prihlásiť sa cez :socialDriver',
22 + 'sign_up_with' => 'Registrovať sa cez :socialDriver',
23 + 'logout' => 'Odhlásenie',
24 +
25 + 'name' => 'Meno',
26 + 'username' => 'Používateľské meno',
27 + 'email' => 'Email',
28 + 'password' => 'Heslo',
29 + 'password_confirm' => 'Potvrdiť heslo',
30 + 'password_hint' => 'Musí mať viac ako 5 znakov',
31 + 'forgot_password' => 'Zabudli ste heslo?',
32 + 'remember_me' => 'Zapamätať si ma',
33 + 'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
34 + 'create_account' => 'Vytvoriť účet',
35 + 'social_login' => 'Sociálne prihlásenie',
36 + 'social_registration' => 'Sociálna registrácia',
37 + 'social_registration_text' => 'Registrovať sa a prihlásiť sa použitím inej služby.',
38 +
39 + 'register_thanks' => 'Ďakujeme zaregistráciu!',
40 + 'register_confirm' => 'Skontrolujte prosím svoj email a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.',
41 + 'registrations_disabled' => 'Registrácie sú momentálne zablokované',
42 + 'registration_email_domain_invalid' => 'Táto emailová doména nemá prístup k tejto aplikácii',
43 + 'register_success' => 'Ďakujeme za registráciu! Teraz ste registrovaný a prihlásený.',
44 +
45 +
46 + /**
47 + * Password Reset
48 + */
49 + 'reset_password' => 'Reset hesla',
50 + 'reset_password_send_instructions' => 'Zadajte svoj email nižšie a bude Vám odoslaný email s odkazom pre reset hesla.',
51 + 'reset_password_send_button' => 'Poslať odkaz na reset hesla',
52 + 'reset_password_sent_success' => 'Odkaz na reset hesla bol poslaný na :email.',
53 + 'reset_password_success' => 'Vaše heslo bolo úspešne resetované.',
54 +
55 + 'email_reset_subject' => 'Reset Vášho :appName hesla',
56 + 'email_reset_text' => 'Tento email Ste dostali pretože sme dostali požiadavku na reset hesla pre Váš účet.',
57 + 'email_reset_not_requested' => 'Ak ste nepožiadali o reset hesla, nemusíte nič robiť.',
58 +
59 +
60 + /**
61 + * Email Confirmation
62 + */
63 + 'email_confirm_subject' => 'Potvrdiť email na :appName',
64 + 'email_confirm_greeting' => 'Ďakujeme za pridanie sa k :appName!',
65 + 'email_confirm_text' => 'Prosím potvrďte Vašu emailovú adresu kliknutím na tlačidlo nižšie:',
66 + 'email_confirm_action' => 'Potvrdiť email',
67 + '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.',
68 + 'email_confirm_success' => 'Váš email bol overený!',
69 + 'email_confirm_resent' => 'Potvrdzujúci email bol poslaný znovu, skontrolujte prosím svoju emailovú schránku.',
70 +
71 + 'email_not_confirmed' => 'Emailová adresa nebola overená',
72 + 'email_not_confirmed_text' => 'Vaša emailová adresa nebola zatiaľ overená.',
73 + 'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v emaili, ktorý bol poslaný krátko po Vašej registrácii.',
74 + 'email_not_confirmed_resend' => 'Ak nemôžete násť email, môžete znova odoslať overovací email odoslaním doleuvedeného formulára.',
75 + 'email_not_confirmed_resend_button' => 'Znova odoslať overovací email',
76 +];
1 +<?php
2 +return [
3 +
4 + /**
5 + * Buttons
6 + */
7 + 'cancel' => 'Zrušiť',
8 + 'confirm' => 'Potvrdiť',
9 + 'back' => 'Späť',
10 + 'save' => 'Uložiť',
11 + 'continue' => 'Pokračovať',
12 + 'select' => 'Vybrať',
13 +
14 + /**
15 + * Form Labels
16 + */
17 + 'name' => 'Meno',
18 + 'description' => 'Popis',
19 + 'role' => 'Rola',
20 +
21 + /**
22 + * Actions
23 + */
24 + 'actions' => 'Akcie',
25 + 'view' => 'Zobraziť',
26 + 'create' => 'Vytvoriť',
27 + 'update' => 'Aktualizovať',
28 + 'edit' => 'Editovať',
29 + 'sort' => 'Zoradiť',
30 + 'move' => 'Presunúť',
31 + 'delete' => 'Zmazať',
32 + 'search' => 'Hľadť',
33 + 'search_clear' => 'Vyčistiť hľadanie',
34 + 'reset' => 'Reset',
35 + 'remove' => 'Odstrániť',
36 +
37 +
38 + /**
39 + * Misc
40 + */
41 + 'deleted_user' => 'Odstránený používateľ',
42 + 'no_activity' => 'Žiadna aktivita na zobrazenie',
43 + 'no_items' => 'Žiadne položky nie sú dostupné',
44 + 'back_to_top' => 'Späť nahor',
45 + 'toggle_details' => 'Prepnúť detaily',
46 +
47 + /**
48 + * Header
49 + */
50 + 'view_profile' => 'Zobraziť profil',
51 + 'edit_profile' => 'Upraviť profil',
52 +
53 + /**
54 + * Email Content
55 + */
56 + '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:',
57 + 'email_rights' => 'Všetky práva vyhradené',
58 +];
1 +<?php
2 +return [
3 +
4 + /**
5 + * Image Manager
6 + */
7 + 'image_select' => 'Vybrať obrázok',
8 + 'image_all' => 'Všetko',
9 + 'image_all_title' => 'Zobraziť všetky obrázky',
10 + 'image_book_title' => 'Zobraziť obrázky nahrané do tejto knihy',
11 + 'image_page_title' => 'Zobraziť obrázky nahrané do tejto stránky',
12 + 'image_search_hint' => 'Hľadať obrázok podľa názvu',
13 + 'image_uploaded' => 'Nahrané :uploadedDate',
14 + 'image_load_more' => 'Načítať viac',
15 + 'image_image_name' => 'Názov obrázka',
16 + '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.',
17 + 'image_select_image' => 'Vybrať obrázok',
18 + 'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie',
19 + 'images_deleted' => 'Obrázky zmazané',
20 + 'image_preview' => 'Náhľad obrázka',
21 + 'image_upload_success' => 'Obrázok úspešne nahraný',
22 + 'image_update_success' => 'Detaily obrázka úspešne aktualizované',
23 + 'image_delete_success' => 'Obrázok úspešne zmazaný'
24 +];
1 +<?php
2 +return [
3 +
4 + /**
5 + * Shared
6 + */
7 + 'recently_created' => 'Nedávno vytvorené',
8 + 'recently_created_pages' => 'Nedávno vytvorené stránky',
9 + 'recently_updated_pages' => 'Nedávno aktualizované stránky',
10 + 'recently_created_chapters' => 'Nedávno vytvorené kapitoly',
11 + 'recently_created_books' => 'Nedávno vytvorené knihy',
12 + 'recently_update' => 'Nedávno aktualizované',
13 + 'recently_viewed' => 'Nedávno zobrazené',
14 + 'recent_activity' => 'Nedávna aktivita',
15 + 'create_now' => 'Vytvoriť teraz',
16 + 'revisions' => 'Revízie',
17 + 'meta_created' => 'Vytvorené :timeLength',
18 + 'meta_created_name' => 'Vytvorené :timeLength používateľom :user',
19 + 'meta_updated' => 'Aktualizované :timeLength',
20 + 'meta_updated_name' => 'Aktualizované :timeLength používateľom :user',
21 + 'x_pages' => ':count stránok',
22 + 'entity_select' => 'Entita vybraná',
23 + 'images' => 'Obrázky',
24 + 'my_recent_drafts' => 'Moje nedávne koncepty',
25 + 'my_recently_viewed' => 'Nedávno mnou zobrazené',
26 + 'no_pages_viewed' => 'Nepozreli ste si žiadne stránky',
27 + 'no_pages_recently_created' => 'Žiadne stránky neboli nedávno vytvorené',
28 + 'no_pages_recently_updated' => 'Žiadne stránky neboli nedávno aktualizované',
29 + 'export' => 'Export',
30 + 'export_html' => 'Contained Web File',
31 + 'export_pdf' => 'PDF súbor',
32 + 'export_text' => 'Súbor s čistým textom',
33 +
34 + /**
35 + * Permissions and restrictions
36 + */
37 + 'permissions' => 'Oprávnenia',
38 + 'permissions_intro' => 'Ak budú tieto oprávnenia povolené, budú mať prioritu pred oprávneniami roly.',
39 + 'permissions_enable' => 'Povoliť vlastné oprávnenia',
40 + 'permissions_save' => 'Uložiť oprávnenia',
41 +
42 + /**
43 + * Search
44 + */
45 + 'search_results' => 'Výsledky hľadania',
46 + 'search_results_page' => 'Výsledky hľadania stránky',
47 + 'search_results_chapter' => 'Výsledky hľadania kapitoly',
48 + 'search_results_book' => 'Výsledky hľadania knihy',
49 + 'search_clear' => 'Vyčistiť hľadanie',
50 + 'search_view_pages' => 'Zobraziť všetky vyhovujúce stránky',
51 + 'search_view_chapters' => 'Zobraziť všetky vyhovujúce kapitoly',
52 + 'search_view_books' => 'Zobraziť všetky vyhovujúce knihy',
53 + 'search_no_pages' => 'Žiadne stránky nevyhovujú tomuto hľadaniu',
54 + 'search_for_term' => 'Hľadať :term',
55 + 'search_page_for_term' => 'Hľadať :term medzi stránkami',
56 + 'search_chapter_for_term' => 'Hľadať :term medzi kapitolami',
57 + 'search_book_for_term' => 'Hľadať :term medzi knihami',
58 +
59 + /**
60 + * Books
61 + */
62 + 'book' => 'Kniha',
63 + 'books' => 'Knihy',
64 + 'books_empty' => 'Žiadne knihy neboli vytvorené',
65 + 'books_popular' => 'Populárne knihy',
66 + 'books_recent' => 'Nedávne knihy',
67 + 'books_popular_empty' => 'Najpopulárnejšie knihy sa objavia tu.',
68 + 'books_create' => 'Vytvoriť novú knihu',
69 + 'books_delete' => 'Zmazať knihu',
70 + 'books_delete_named' => 'Zmazať knihu :bookName',
71 + 'books_delete_explain' => 'Toto zmaže knihu s názvom \':bookName\', všetky stránky a kapitoly budú odstránené.',
72 + 'books_delete_confirmation' => 'Ste si istý, že chcete zmazať túto knihu?',
73 + 'books_edit' => 'Upraviť knihu',
74 + 'books_edit_named' => 'Upraviť knihu :bookName',
75 + 'books_form_book_name' => 'Názov knihy',
76 + 'books_save' => 'Uložiť knihu',
77 + 'books_permissions' => 'Oprávnenia knihy',
78 + 'books_permissions_updated' => 'Oprávnenia knihy aktualizované',
79 + 'books_empty_contents' => 'Pre túto knihu neboli vytvorené žiadne stránky alebo kapitoly.',
80 + 'books_empty_create_page' => 'Vytvoriť novú stránku',
81 + 'books_empty_or' => 'alebo',
82 + 'books_empty_sort_current_book' => 'Zoradiť aktuálnu knihu',
83 + 'books_empty_add_chapter' => 'Pridať kapitolu',
84 + 'books_permissions_active' => 'Oprávnenia knihy aktívne',
85 + 'books_search_this' => 'Hľadať v tejto knihe',
86 + 'books_navigation' => 'Navigácia knihy',
87 + 'books_sort' => 'Zoradiť obsah knihy',
88 + 'books_sort_named' => 'Zoradiť knihu :bookName',
89 + 'books_sort_show_other' => 'Zobraziť ostatné knihy',
90 + 'books_sort_save' => 'Uložiť nové zoradenie',
91 +
92 + /**
93 + * Chapters
94 + */
95 + 'chapter' => 'Kapitola',
96 + 'chapters' => 'Kapitoly',
97 + 'chapters_popular' => 'Populárne kapitoly',
98 + 'chapters_new' => 'Nová kapitola',
99 + 'chapters_create' => 'Vytvoriť novú kapitolu',
100 + 'chapters_delete' => 'Zmazať kapitolu',
101 + 'chapters_delete_named' => 'Zmazať kapitolu :chapterName',
102 + 'chapters_delete_explain' => 'Toto zmaže kapitolu menom \':chapterName\', všetky stránky budú ostránené
103 + a pridané priamo do rodičovskej knihy.',
104 + 'chapters_delete_confirm' => 'Ste si istý, že chcete zmazať túto kapitolu?',
105 + 'chapters_edit' => 'Upraviť kapitolu',
106 + 'chapters_edit_named' => 'Upraviť kapitolu :chapterName',
107 + 'chapters_save' => 'Uložiť kapitolu',
108 + 'chapters_move' => 'Presunúť kapitolu',
109 + 'chapters_move_named' => 'Presunúť kapitolu :chapterName',
110 + 'chapter_move_success' => 'Kapitola presunutá do :bookName',
111 + 'chapters_permissions' => 'Oprávnenia kapitoly',
112 + 'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.',
113 + 'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne',
114 + 'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované',
115 +
116 + /**
117 + * Pages
118 + */
119 + 'page' => 'Stránka',
120 + 'pages' => 'Stránky',
121 + 'pages_popular' => 'Populárne stránky',
122 + 'pages_new' => 'Nová stránka',
123 + 'pages_attachments' => 'Prílohy',
124 + 'pages_navigation' => 'Navigácia',
125 + 'pages_delete' => 'Zmazať stránku',
126 + 'pages_delete_named' => 'Zmazať stránku :pageName',
127 + 'pages_delete_draft_named' => 'Zmazať koncept :pageName',
128 + 'pages_delete_draft' => 'Zmazať koncept',
129 + 'pages_delete_success' => 'Stránka zmazaná',
130 + 'pages_delete_draft_success' => 'Koncept stránky zmazaný',
131 + 'pages_delete_confirm' => 'Ste si istý, že chcete zmazať túto stránku?',
132 + 'pages_delete_draft_confirm' => 'Ste si istý, že chcete zmazať tento koncept stránky?',
133 + 'pages_editing_named' => 'Upraviť stránku :pageName',
134 + 'pages_edit_toggle_header' => 'Prepnúť hlavičku',
135 + 'pages_edit_save_draft' => 'Uložiť koncept',
136 + 'pages_edit_draft' => 'Upraviť koncept stránky',
137 + 'pages_editing_draft' => 'Upravuje sa koncept',
138 + 'pages_editing_page' => 'Upravuje sa stránka',
139 + 'pages_edit_draft_save_at' => 'Koncept uložený pod ',
140 + 'pages_edit_delete_draft' => 'Uložiť koncept',
141 + 'pages_edit_discard_draft' => 'Zrušiť koncept',
142 + 'pages_edit_set_changelog' => 'Nastaviť záznam zmien',
143 + 'pages_edit_enter_changelog_desc' => 'Zadajte krátky popis zmien, ktoré ste urobili',
144 + 'pages_edit_enter_changelog' => 'Zadať záznam zmien',
145 + 'pages_save' => 'Uložiť stránku',
146 + 'pages_title' => 'Titulok stránky',
147 + 'pages_name' => 'Názov stránky',
148 + 'pages_md_editor' => 'Editor',
149 + 'pages_md_preview' => 'Náhľad',
150 + 'pages_md_insert_image' => 'Vložiť obrázok',
151 + 'pages_md_insert_link' => 'Vložiť odkaz na entitu',
152 + 'pages_not_in_chapter' => 'Stránka nie je v kapitole',
153 + 'pages_move' => 'Presunúť stránku',
154 + 'pages_move_success' => 'Stránka presunutá do ":parentName"',
155 + 'pages_permissions' => 'Oprávnenia stránky',
156 + 'pages_permissions_success' => 'Oprávnenia stránky aktualizované',
157 + 'pages_revisions' => 'Revízie stránky',
158 + 'pages_revisions_named' => 'Revízie stránky :pageName',
159 + 'pages_revision_named' => 'Revízia stránky :pageName',
160 + 'pages_revisions_created_by' => 'Vytvoril',
161 + 'pages_revisions_date' => 'Dátum revízie',
162 + 'pages_revisions_changelog' => 'Záznam zmien',
163 + 'pages_revisions_changes' => 'Zmeny',
164 + 'pages_revisions_current' => 'Aktuálna verzia',
165 + 'pages_revisions_preview' => 'Náhľad',
166 + 'pages_revisions_restore' => 'Obnoviť',
167 + 'pages_revisions_none' => 'Táto stránka nemá žiadne revízie',
168 + 'pages_copy_link' => 'Kopírovať odkaz',
169 + 'pages_permissions_active' => 'Oprávnienia stránky aktívne',
170 + 'pages_initial_revision' => 'Prvé zverejnenie',
171 + 'pages_initial_name' => 'Nová stránka',
172 + 'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.',
173 + 'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.',
174 + 'pages_draft_edit_active' => [
175 + 'start_a' => ':count používateľov začalo upravovať túto stránku',
176 + 'start_b' => ':userName začal upravovať túto stránku',
177 + 'time_a' => 'odkedy boli stránky naposledy aktualizované',
178 + 'time_b' => 'za posledných :minCount minút',
179 + 'message' => ':start :time. Dávajte pozor aby ste si navzájom neprepísali zmeny!',
180 + ],
181 + 'pages_draft_discarded' => 'Koncept ostránený, aktuálny obsah stránky bol nahraný do editora',
182 +
183 + /**
184 + * Editor sidebar
185 + */
186 + 'page_tags' => 'Štítky stránok',
187 + 'tag' => 'Štítok',
188 + 'tags' => 'Štítky',
189 + 'tag_value' => 'Hodnota štítku (Voliteľné)',
190 + '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.",
191 + 'tags_add' => 'Pridať ďalší štítok',
192 + 'attachments' => 'Prílohy',
193 + '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.',
194 + 'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.',
195 + 'attachments_items' => 'Priložené položky',
196 + 'attachments_upload' => 'Nahrať súbor',
197 + 'attachments_link' => 'Priložiť odkaz',
198 + 'attachments_set_link' => 'Nastaviť odkaz',
199 + 'attachments_delete_confirm' => 'Kliknite znova na zmazať pre potvrdenie zmazania prílohy.',
200 + 'attachments_dropzone' => 'Presuňte súbory alebo klinknite sem pre priloženie súboru',
201 + 'attachments_no_files' => 'Žiadne súbory neboli nahrané',
202 + '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.',
203 + 'attachments_link_name' => 'Názov odkazu',
204 + 'attachment_link' => 'Odkaz na prílohu',
205 + 'attachments_link_url' => 'Odkaz na súbor',
206 + 'attachments_link_url_hint' => 'Url stránky alebo súboru',
207 + 'attach' => 'Priložiť',
208 + 'attachments_edit_file' => 'Upraviť súbor',
209 + 'attachments_edit_file_name' => 'Názov súboru',
210 + 'attachments_edit_drop_upload' => 'Presuňte súbory sem alebo klinknite pre nahranie a prepis',
211 + 'attachments_order_updated' => 'Poradie príloh aktualizované',
212 + 'attachments_updated_success' => 'Detaily prílohy aktualizované',
213 + 'attachments_deleted' => 'Príloha zmazaná',
214 + 'attachments_file_uploaded' => 'Súbor úspešne nahraný',
215 + 'attachments_file_updated' => 'Súbor úspešne aktualizovaný',
216 + 'attachments_link_attached' => 'Odkaz úspešne pripojený k stránke',
217 +
218 + /**
219 + * Profile View
220 + */
221 + 'profile_user_for_x' => 'Používateľ už :time',
222 + 'profile_created_content' => 'Vytvorený obsah',
223 + 'profile_not_created_pages' => ':userName nevytvoril žiadne stránky',
224 + 'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly',
225 + 'profile_not_created_books' => ':userName nevytvoril žiadne knihy',
226 +];
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Error text strings.
7 + */
8 +
9 + // Permissions
10 + 'permission' => 'Nemáte oprávnenie pre prístup k požadovanej stránke.',
11 + 'permissionJson' => 'Nemáte oprávnenie pre vykonanie požadovaného úkonu.',
12 +
13 + // Auth
14 + 'error_user_exists_different_creds' => 'Používateľ s emailom :email už existuje, ale s inými údajmi.',
15 + 'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.',
16 + 'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.',
17 + 'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.',
18 + 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
19 + 'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
20 + 'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
21 + 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
22 + 'social_no_action_defined' => 'Nebola definovaná žiadna akcia',
23 + 'social_account_in_use' => 'Tento :socialAccount účet sa už používa, skúste sa prihlásiť pomocou možnosti :socialAccount.',
24 + '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.',
25 + 'social_account_existing' => 'Tento :socialAccount účet je už spojený s Vaším profilom.',
26 + 'social_account_already_used_existing' => 'Tento :socialAccount účet už používa iný používateľ.',
27 + 'social_account_not_used' => 'Tento :socialAccount účet nie je spojený so žiadnym používateľom. Pripojte ho prosím v nastaveniach Vášho profilu. ',
28 + 'social_account_register_instructions' => 'Ak zatiaľ nemáte účet, môžete sa registrovať pomocou možnosti :socialAccount.',
29 + 'social_driver_not_found' => 'Ovládač socialnych sietí nebol nájdený',
30 + 'social_driver_not_configured' => 'Nastavenia Vášho :socialAccount účtu nie sú správne.',
31 +
32 + // System
33 + 'path_not_writable' => 'Do cesty :filePath sa nedá nahrávať. Uistite sa, že je zapisovateľná serverom.',
34 + 'cannot_get_image_from_url' => 'Nedá sa získať obrázok z :url',
35 + 'cannot_create_thumbs' => 'Server nedokáže vytvoriť náhľady. Skontrolujte prosím, či máte nainštalované GD rozšírenie PHP.',
36 + 'server_upload_limit' => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',
37 + 'image_upload_error' => 'Pri nahrávaní obrázka nastala chyba',
38 +
39 + // Attachments
40 + 'attachment_page_mismatch' => 'Page mismatch during attachment update',
41 +
42 + // Pages
43 + 'page_draft_autosave_fail' => 'Koncept nemohol byť uložený. Uistite sa, že máte pripojenie k internetu pre uložením tejto stránky',
44 +
45 + // Entities
46 + 'entity_not_found' => 'Entita nenájdená',
47 + 'book_not_found' => 'Kniha nenájdená',
48 + 'page_not_found' => 'Stránka nenájdená',
49 + 'chapter_not_found' => 'Kapitola nenájdená',
50 + 'selected_book_not_found' => 'Vybraná kniha nebola nájdená',
51 + 'selected_book_chapter_not_found' => 'Vybraná kniha alebo kapitola nebola nájdená',
52 + 'guests_cannot_save_drafts' => 'Hosť nemôže ukladať koncepty',
53 +
54 + // Users
55 + 'users_cannot_delete_only_admin' => 'Nemôžete zmazať posledného správcu',
56 + 'users_cannot_delete_guest' => 'Nemôžete zmazať hosťa',
57 +
58 + // Roles
59 + 'role_cannot_be_edited' => 'Táto rola nemôže byť upravovaná',
60 + 'role_system_cannot_be_deleted' => 'Táto rola je systémová rola a nemôže byť zmazaná',
61 + 'role_registration_default_cannot_delete' => 'Táto rola nemôže byť zmazaná, pretože je nastavená ako prednastavená rola pri registrácii',
62 +
63 + // Error pages
64 + '404_page_not_found' => 'Stránka nenájdená',
65 + 'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.',
66 + 'return_home' => 'Vrátiť sa domov',
67 + 'error_occurred' => 'Nastala chyba',
68 + 'app_down' => ':appName je momentálne nedostupná',
69 + 'back_soon' => 'Čoskoro bude opäť dostupná.',
70 +];
1 +<?php
2 +
3 +return [
4 +
5 + /*
6 + |--------------------------------------------------------------------------
7 + | Pagination Language Lines
8 + |--------------------------------------------------------------------------
9 + |
10 + | The following language lines are used by the paginator library to build
11 + | the simple pagination links. You are free to change them to anything
12 + | you want to customize your views to better match your application.
13 + |
14 + */
15 +
16 + 'previous' => '&laquo; Predchádzajúca',
17 + 'next' => 'Ďalšia &raquo;',
18 +
19 +];
1 +<?php
2 +
3 +return [
4 +
5 + /*
6 + |--------------------------------------------------------------------------
7 + | Password Reminder Language Lines
8 + |--------------------------------------------------------------------------
9 + |
10 + | The following language lines are the default lines which match reasons
11 + | that are given by the password broker for a password update attempt
12 + | has failed, such as for an invalid token or invalid new password.
13 + |
14 + */
15 +
16 + 'password' => 'Heslo musí obsahovať aspoň šesť znakov a musí byť rovnaké ako potvrdzujúce.',
17 + 'user' => "Nenašli sme používateľa s takou emailovou adresou.",
18 + 'token' => 'Tento token pre reset hesla je neplatný.',
19 + 'sent' => 'Poslali sme Vám email s odkazom na reset hesla!',
20 + 'reset' => 'Vaše heslo bolo resetované!',
21 +
22 +];
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Settings text strings
7 + * Contains all text strings used in the general settings sections of BookStack
8 + * including users and roles.
9 + */
10 +
11 + 'settings' => 'Nastavenia',
12 + 'settings_save' => 'Uložiť nastavenia',
13 + 'settings_save_success' => 'Nastavenia uložené',
14 +
15 + /**
16 + * App settings
17 + */
18 +
19 + 'app_settings' => 'Nastavenia aplikácie',
20 + 'app_name' => 'Názov aplikácia',
21 + 'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.',
22 + 'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?',
23 + 'app_public_viewing' => 'Povoliť verejné zobrazenie?',
24 + 'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?',
25 + '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é.',
26 + 'app_editor' => 'Editor stránky',
27 + 'app_editor_desc' => 'Vyberte editor, ktorý bude používaný všetkými používateľmi na editáciu stránok.',
28 + 'app_custom_html' => 'Vlastný HTML obsah hlavičky',
29 + 'app_custom_html_desc' => 'Všetok text pridaný sem bude vložený naspodok <head> sekcie na každej stránke. Môže sa to zísť pri zmene štýlu alebo pre pridanie analytického kódu.',
30 + 'app_logo' => 'Logo aplikácie',
31 + 'app_logo_desc' => 'Tento obrázok by mal mať 43px na výšku. <br>Veľké obrázky budú preškálované na menší rozmer.',
32 + 'app_primary_color' => 'Primárna farba pre aplikáciu',
33 + 'app_primary_color_desc' => 'Toto by mala byť hodnota v hex tvare. <br>Nechajte prázdne ak chcete použiť prednastavenú farbu.',
34 +
35 + /**
36 + * Registration settings
37 + */
38 +
39 + 'reg_settings' => 'Nastavenia registrácie',
40 + 'reg_allow' => 'Povoliť registráciu?',
41 + 'reg_default_role' => 'Prednastavená používateľská rola po registrácii',
42 + 'reg_confirm_email' => 'Vyžadovať overenie emailu?',
43 + 'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.',
44 + 'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu',
45 + '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. <br> Používatelia si budú môcť po úspešnej registrácii zmeniť svoju emailovú adresu.',
46 + 'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia',
47 +
48 + /**
49 + * Role settings
50 + */
51 +
52 + 'roles' => 'Roly',
53 + 'role_user_roles' => 'Používateľské roly',
54 + 'role_create' => 'Vytvoriť novú rolu',
55 + 'role_create_success' => 'Rola úspešne vytvorená',
56 + 'role_delete' => 'Zmazať rolu',
57 + 'role_delete_confirm' => 'Toto zmaže rolu menom \':roleName\'.',
58 + '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.',
59 + 'role_delete_no_migration' => "Nemigrovať používateľov",
60 + 'role_delete_sure' => 'Ste si istý, že chcete zmazať túto rolu?',
61 + 'role_delete_success' => 'Rola úspešne zmazaná',
62 + 'role_edit' => 'Upraviť rolu',
63 + 'role_details' => 'Detaily roly',
64 + 'role_name' => 'Názov roly',
65 + 'role_desc' => 'Krátky popis roly',
66 + 'role_system' => 'Systémové oprávnenia',
67 + 'role_manage_users' => 'Spravovať používateľov',
68 + 'role_manage_roles' => 'Spravovať role a oprávnenia rolí',
69 + 'role_manage_entity_permissions' => 'Spravovať všetky oprávnenia kníh, kapitol a stránok',
70 + 'role_manage_own_entity_permissions' => 'Spravovať oprávnenia vlastných kníh, kapitol a stránok',
71 + 'role_manage_settings' => 'Spravovať nastavenia aplikácie',
72 + 'role_asset' => 'Oprávnenia majetku',
73 + '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.',
74 + 'role_all' => 'Všetko',
75 + 'role_own' => 'Vlastné',
76 + 'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané',
77 + 'role_save' => 'Uložiť rolu',
78 + 'role_update_success' => 'Roly úspešne aktualizované',
79 + 'role_users' => 'Používatelia s touto rolou',
80 + 'role_users_none' => 'Žiadni používatelia nemajú priradenú túto rolu',
81 +
82 + /**
83 + * Users
84 + */
85 +
86 + 'users' => 'Používatelia',
87 + 'user_profile' => 'Profil používateľa',
88 + 'users_add_new' => 'Pridať nového používateľa',
89 + 'users_search' => 'Hľadať medzi používateľmi',
90 + 'users_role' => 'Používateľské roly',
91 + 'users_external_auth_id' => 'Externé autentifikačné ID',
92 + 'users_password_warning' => 'Pole nižšie vyplňte iba ak chcete zmeniť heslo:',
93 + '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.',
94 + 'users_delete' => 'Zmazať používateľa',
95 + 'users_delete_named' => 'Zmazať používateľa :userName',
96 + 'users_delete_warning' => ' Toto úplne odstráni používateľa menom \':userName\' zo systému.',
97 + 'users_delete_confirm' => 'Ste si istý, že chcete zmazať tohoto používateľa?',
98 + 'users_delete_success' => 'Používateľ úspešne zmazaný',
99 + 'users_edit' => 'Upraviť používateľa',
100 + 'users_edit_profile' => 'Upraviť profil',
101 + 'users_edit_success' => 'Používateľ úspešne upravený',
102 + 'users_avatar' => 'Avatar používateľa',
103 + 'users_avatar_desc' => 'Tento obrázok by mal byť štvorec s rozmerom približne 256px.',
104 + 'users_preferred_language' => 'Preferovaný jazyk',
105 + 'users_social_accounts' => 'Sociálne účty',
106 + '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.',
107 + 'users_social_connect' => 'Pripojiť účet',
108 + 'users_social_disconnect' => 'Odpojiť účet',
109 + 'users_social_connected' => ':socialAccount účet bol úspešne pripojený k Vášmu profilu.',
110 + 'users_social_disconnected' => ':socialAccount účet bol úspešne odpojený od Vášho profilu.',
111 +];
1 +<?php
2 +
3 +return [
4 +
5 + /*
6 + |--------------------------------------------------------------------------
7 + | Validation Language Lines
8 + |--------------------------------------------------------------------------
9 + |
10 + | The following language lines contain the default error messages used by
11 + | the validator class. Some of these rules have multiple versions such
12 + | as the size rules. Feel free to tweak each of these messages here.
13 + |
14 + */
15 +
16 + 'accepted' => ':attribute musí byť akceptovaný.',
17 + 'active_url' => ':attribute nie je platná URL.',
18 + 'after' => ':attribute musí byť dátum po :date.',
19 + 'alpha' => ':attribute môže obsahovať iba písmená.',
20 + 'alpha_dash' => ':attribute môže obsahovať iba písmená, čísla a pomlčky.',
21 + 'alpha_num' => ':attribute môže obsahovať iba písmená a čísla.',
22 + 'array' => ':attribute musí byť pole.',
23 + 'before' => ':attribute musí byť dátum pred :date.',
24 + 'between' => [
25 + 'numeric' => ':attribute musí byť medzi :min a :max.',
26 + 'file' => ':attribute musí byť medzi :min a :max kilobajtmi.',
27 + 'string' => ':attribute musí byť medzi :min a :max znakmi.',
28 + 'array' => ':attribute musí byť medzi :min a :max položkami.',
29 + ],
30 + 'boolean' => ':attribute pole musí byť true alebo false.',
31 + 'confirmed' => ':attribute potvrdenie nesedí.',
32 + 'date' => ':attribute nie je platný dátum.',
33 + 'date_format' => ':attribute nesedí s formátom :format.',
34 + 'different' => ':attribute a :other musia byť rozdielne.',
35 + 'digits' => ':attribute musí mať :digits číslic.',
36 + 'digits_between' => ':attribute musí mať medzi :min a :max číslicami.',
37 + 'email' => ':attribute musí byť platná emailová adresa.',
38 + 'filled' => 'Políčko :attribute je povinné.',
39 + 'exists' => 'Vybraný :attribute nie je platný.',
40 + 'image' => ':attribute musí byť obrázok.',
41 + 'in' => 'Vybraný :attribute je neplatný.',
42 + 'integer' => ':attribute musí byť celé číslo.',
43 + 'ip' => ':attribute musí byť platná IP adresa.',
44 + 'max' => [
45 + 'numeric' => ':attribute nesmie byť väčší ako :max.',
46 + 'file' => ':attribute nesmie byť väčší ako :max kilobajtov.',
47 + 'string' => ':attribute nesmie byť dlhší ako :max znakov.',
48 + 'array' => ':attribute nesmie mať viac ako :max položiek.',
49 + ],
50 + 'mimes' => ':attribute musí byť súbor typu: :values.',
51 + 'min' => [
52 + 'numeric' => ':attribute musí byť aspoň :min.',
53 + 'file' => ':attribute musí mať aspoň :min kilobajtov.',
54 + 'string' => ':attribute musí mať aspoň :min znakov.',
55 + 'array' => ':attribute musí mať aspoň :min položiek.',
56 + ],
57 + 'not_in' => 'Vybraný :attribute je neplatný.',
58 + 'numeric' => ':attribute musí byť číslo.',
59 + 'regex' => ':attribute formát je neplatný.',
60 + 'required' => 'Políčko :attribute je povinné.',
61 + 'required_if' => 'Políčko :attribute je povinné ak :other je :value.',
62 + 'required_with' => 'Políčko :attribute je povinné ak :values existuje.',
63 + 'required_with_all' => 'Políčko :attribute je povinné ak :values existuje.',
64 + 'required_without' => 'Políčko :attribute je povinné aj :values neexistuje.',
65 + 'required_without_all' => 'Políčko :attribute je povinné ak ani jedno z :values neexistuje.',
66 + 'same' => ':attribute a :other musia byť rovnaké.',
67 + 'size' => [
68 + 'numeric' => ':attribute musí byť :size.',
69 + 'file' => ':attribute musí mať :size kilobajtov.',
70 + 'string' => ':attribute musí mať :size znakov.',
71 + 'array' => ':attribute musí obsahovať :size položiek.',
72 + ],
73 + 'string' => ':attribute musí byť reťazec.',
74 + 'timezone' => ':attribute musí byť plantá časová zóna.',
75 + 'unique' => ':attribute je už použité.',
76 + 'url' => ':attribute formát je neplatný.',
77 +
78 + /*
79 + |--------------------------------------------------------------------------
80 + | Custom Validation Language Lines
81 + |--------------------------------------------------------------------------
82 + |
83 + | Here you may specify custom validation messages for attributes using the
84 + | convention "attribute.rule" to name the lines. This makes it quick to
85 + | specify a specific custom language line for a given attribute rule.
86 + |
87 + */
88 +
89 + 'custom' => [
90 + 'password-confirm' => [
91 + 'required_with' => 'Vyžaduje sa potvrdenie hesla',
92 + ],
93 + ],
94 +
95 + /*
96 + |--------------------------------------------------------------------------
97 + | Custom Validation Attributes
98 + |--------------------------------------------------------------------------
99 + |
100 + | The following language lines are used to swap attribute place-holders
101 + | with something more reader friendly such as E-Mail Address instead
102 + | of "email". This simply helps us make messages a little cleaner.
103 + |
104 + */
105 +
106 + 'attributes' => [],
107 +
108 +];
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
47 </a> 47 </a>
48 </div> 48 </div>
49 <div class="col-lg-4 col-sm-3 text-center"> 49 <div class="col-lg-4 col-sm-3 text-center">
50 - <form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box"> 50 + <form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
51 <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> 51 <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
52 <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button> 52 <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
53 </form> 53 </form>
......
...@@ -50,15 +50,15 @@ ...@@ -50,15 +50,15 @@
50 </div> 50 </div>
51 51
52 52
53 - <div class="container" id="book-dashboard" ng-controller="BookShowController" book-id="{{ $book->id }}"> 53 + <div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
54 <div class="row"> 54 <div class="row">
55 <div class="col-md-7"> 55 <div class="col-md-7">
56 56
57 <h1>{{$book->name}}</h1> 57 <h1>{{$book->name}}</h1>
58 - <div class="book-content" ng-show="!searching"> 58 + <div class="book-content" v-if="!searching">
59 - <p class="text-muted" ng-non-bindable>{{$book->description}}</p> 59 + <p class="text-muted" v-pre>{{$book->description}}</p>
60 60
61 - <div class="page-list" ng-non-bindable> 61 + <div class="page-list" v-pre>
62 <hr> 62 <hr>
63 @if(count($bookChildren) > 0) 63 @if(count($bookChildren) > 0)
64 @foreach($bookChildren as $childElement) 64 @foreach($bookChildren as $childElement)
...@@ -81,12 +81,12 @@ ...@@ -81,12 +81,12 @@
81 @include('partials.entity-meta', ['entity' => $book]) 81 @include('partials.entity-meta', ['entity' => $book])
82 </div> 82 </div>
83 </div> 83 </div>
84 - <div class="search-results" ng-cloak ng-show="searching"> 84 + <div class="search-results" v-cloak v-if="searching">
85 - <h3 class="text-muted">{{ trans('entities.search_results') }} <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3> 85 + <h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
86 - <div ng-if="!searchResults"> 86 + <div v-if="!searchResults">
87 @include('partials/loading-icon') 87 @include('partials/loading-icon')
88 </div> 88 </div>
89 - <div ng-bind-html="searchResults"></div> 89 + <div v-html="searchResults"></div>
90 </div> 90 </div>
91 91
92 92
...@@ -94,6 +94,7 @@ ...@@ -94,6 +94,7 @@
94 94
95 <div class="col-md-4 col-md-offset-1"> 95 <div class="col-md-4 col-md-offset-1">
96 <div class="margin-top large"></div> 96 <div class="margin-top large"></div>
97 +
97 @if($book->restricted) 98 @if($book->restricted)
98 <p class="text-muted"> 99 <p class="text-muted">
99 @if(userCan('restrictions-manage', $book)) 100 @if(userCan('restrictions-manage', $book))
...@@ -103,14 +104,16 @@ ...@@ -103,14 +104,16 @@
103 @endif 104 @endif
104 </p> 105 </p>
105 @endif 106 @endif
107 +
106 <div class="search-box"> 108 <div class="search-box">
107 - <form ng-submit="searchBook($event)"> 109 + <form v-on:submit="searchBook">
108 - <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}"> 110 + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
109 <button type="submit"><i class="zmdi zmdi-search"></i></button> 111 <button type="submit"><i class="zmdi zmdi-search"></i></button>
110 - <button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> 112 + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
111 </form> 113 </form>
112 </div> 114 </div>
113 - <div class="activity anim fadeIn"> 115 +
116 + <div class="activity">
114 <h3>{{ trans('entities.recent_activity') }}</h3> 117 <h3>{{ trans('entities.recent_activity') }}</h3>
115 @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) 118 @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
116 </div> 119 </div>
......
1 <div class="breadcrumbs"> 1 <div class="breadcrumbs">
2 + @if (userCan('view', $chapter->book))
2 <a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a> 3 <a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
3 <span class="sep">&raquo;</span> 4 <span class="sep">&raquo;</span>
5 + @endif
4 <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a> 6 <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
5 </div> 7 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -47,40 +47,50 @@ ...@@ -47,40 +47,50 @@
47 </div> 47 </div>
48 48
49 49
50 - <div class="container" ng-non-bindable> 50 + <div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter">
51 <div class="row"> 51 <div class="row">
52 - <div class="col-md-8"> 52 + <div class="col-md-7">
53 <h1>{{ $chapter->name }}</h1> 53 <h1>{{ $chapter->name }}</h1>
54 - <p class="text-muted">{{ $chapter->description }}</p> 54 + <div class="chapter-content" v-if="!searching">
55 + <p class="text-muted">{{ $chapter->description }}</p>
55 56
56 - @if(count($pages) > 0) 57 + @if(count($pages) > 0)
57 - <div class="page-list"> 58 + <div class="page-list">
58 - <hr>
59 - @foreach($pages as $page)
60 - @include('pages/list-item', ['page' => $page])
61 <hr> 59 <hr>
62 - @endforeach 60 + @foreach($pages as $page)
63 - </div> 61 + @include('pages/list-item', ['page' => $page])
64 - @else 62 + <hr>
65 - <hr> 63 + @endforeach
66 - <p class="text-muted">{{ trans('entities.chapters_empty') }}</p> 64 + </div>
67 - <p> 65 + @else
68 - @if(userCan('page-create', $chapter)) 66 + <hr>
69 - <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a> 67 + <p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
70 - @endif 68 + <p>
71 - @if(userCan('page-create', $chapter) && userCan('book-update', $book)) 69 + @if(userCan('page-create', $chapter))
72 - &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp; 70 + <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
73 - @endif 71 + @endif
74 - @if(userCan('book-update', $book)) 72 + @if(userCan('page-create', $chapter) && userCan('book-update', $book))
75 - <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a> 73 + &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
76 - @endif 74 + @endif
77 - </p> 75 + @if(userCan('book-update', $book))
78 - <hr> 76 + <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
79 - @endif 77 + @endif
78 + </p>
79 + <hr>
80 + @endif
80 81
81 - @include('partials.entity-meta', ['entity' => $chapter]) 82 + @include('partials.entity-meta', ['entity' => $chapter])
83 + </div>
84 +
85 + <div class="search-results" v-cloak v-if="searching">
86 + <h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
87 + <div v-if="!searchResults">
88 + @include('partials/loading-icon')
89 + </div>
90 + <div v-html="searchResults"></div>
91 + </div>
82 </div> 92 </div>
83 - <div class="col-md-3 col-md-offset-1"> 93 + <div class="col-md-4 col-md-offset-1">
84 <div class="margin-top large"></div> 94 <div class="margin-top large"></div>
85 @if($book->restricted || $chapter->restricted) 95 @if($book->restricted || $chapter->restricted)
86 <div class="text-muted"> 96 <div class="text-muted">
...@@ -105,7 +115,16 @@ ...@@ -105,7 +115,16 @@
105 </div> 115 </div>
106 @endif 116 @endif
107 117
118 + <div class="search-box">
119 + <form v-on:submit="searchBook">
120 + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}">
121 + <button type="submit"><i class="zmdi zmdi-search"></i></button>
122 + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
123 + </form>
124 + </div>
125 +
108 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) 126 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
127 +
109 </div> 128 </div>
110 </div> 129 </div>
111 </div> 130 </div>
......
1 <div class="breadcrumbs"> 1 <div class="breadcrumbs">
2 - <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a> 2 + @if (userCan('view', $page->book))
3 - @if($page->hasChapter()) 3 + <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
4 <span class="sep">&raquo;</span> 4 <span class="sep">&raquo;</span>
5 + @endif
6 + @if($page->hasChapter() && userCan('view', $page->chapter))
5 <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button"> 7 <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
6 <i class="zmdi zmdi-collection-bookmark"></i> 8 <i class="zmdi zmdi-collection-bookmark"></i>
7 {{ $page->chapter->getShortName() }} 9 {{ $page->chapter->getShortName() }}
8 </a> 10 </a>
11 + <span class="sep">&raquo;</span>
9 @endif 12 @endif
10 - <span class="sep">&raquo;</span>
11 <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a> 13 <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
12 </div> 14 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
19 19
20 <table class="table"> 20 <table class="table">
21 <tr> 21 <tr>
22 + <th width="3%">{{ trans('entities.pages_revisions_number') }}</th>
22 <th width="23%">{{ trans('entities.pages_name') }}</th> 23 <th width="23%">{{ trans('entities.pages_name') }}</th>
23 <th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th> 24 <th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
24 <th width="15%">{{ trans('entities.pages_revisions_date') }}</th> 25 <th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
...@@ -27,6 +28,7 @@ ...@@ -27,6 +28,7 @@
27 </tr> 28 </tr>
28 @foreach($page->revisions as $index => $revision) 29 @foreach($page->revisions as $index => $revision)
29 <tr> 30 <tr>
31 + <td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
30 <td>{{ $revision->name }}</td> 32 <td>{{ $revision->name }}</td>
31 <td style="line-height: 0;"> 33 <td style="line-height: 0;">
32 @if($revision->createdBy) 34 @if($revision->createdBy)
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
3 3
4 @if(isset($page) && $page->tags->count() > 0) 4 @if(isset($page) && $page->tags->count() > 0)
5 <div class="tag-display"> 5 <div class="tag-display">
6 - <h6 class="text-muted">Page Tags</h6> 6 + <h6 class="text-muted">{{ trans('entities.page_tags') }}</h6>
7 <table> 7 <table>
8 <tbody> 8 <tbody>
9 @foreach($page->tags as $tag) 9 @foreach($page->tags as $tag)
10 <tr class="tag"> 10 <tr class="tag">
11 - <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td> 11 + <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
12 - @if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif 12 + @if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
13 </tr> 13 </tr>
14 @endforeach 14 @endforeach
15 </tbody> 15 </tbody>
...@@ -39,8 +39,10 @@ ...@@ -39,8 +39,10 @@
39 39
40 <h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6> 40 <h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6>
41 <ul class="sidebar-page-list menu"> 41 <ul class="sidebar-page-list menu">
42 - <li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
43 42
43 + @if (userCan('view', $book))
44 + <li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
45 + @endif
44 46
45 @foreach($sidebarTree as $bookChild) 47 @foreach($sidebarTree as $bookChild)
46 <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}"> 48 <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
......
1 <p class="text-muted small"> 1 <p class="text-muted small">
2 + @if ($entity->isA('page')) {{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> @endif
2 @if ($entity->createdBy) 3 @if ($entity->createdBy)
3 - {!! trans('entities.meta_created_name', ['timeLength' => $entity->created_at->diffForHumans(), 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"]) !!} 4 + {!! trans('entities.meta_created_name', [
5 + 'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
6 + 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"
7 + ]) !!}
4 @else 8 @else
5 - {{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }} 9 + <span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
6 @endif 10 @endif
7 <br> 11 <br>
8 @if ($entity->updatedBy) 12 @if ($entity->updatedBy)
9 - {!! trans('entities.meta_updated_name', ['timeLength' => $entity->updated_at->diffForHumans(), 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"]) !!} 13 + {!! trans('entities.meta_updated_name', [
14 + 'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
15 + 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"
16 + ]) !!}
10 @else 17 @else
11 - {{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }} 18 + <span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
12 @endif 19 @endif
13 </p> 20 </p>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -2,59 +2,212 @@ ...@@ -2,59 +2,212 @@
2 2
3 @section('content') 3 @section('content')
4 4
5 + <input type="hidden" name="searchTerm" value="{{$searchTerm}}">
6 +
7 +<div id="search-system">
8 +
5 <div class="faded-small toolbar"> 9 <div class="faded-small toolbar">
6 <div class="container"> 10 <div class="container">
7 <div class="row"> 11 <div class="row">
8 <div class="col-sm-12 faded"> 12 <div class="col-sm-12 faded">
9 <div class="breadcrumbs"> 13 <div class="breadcrumbs">
10 - <a href="{{ baseUrl("/search/all?term={$searchTerm}") }}" class="text-button"><i class="zmdi zmdi-search"></i>{{ $searchTerm }}</a> 14 + <a href="{{ baseUrl("/search?term=" . urlencode($searchTerm)) }}" class="text-button"><i class="zmdi zmdi-search"></i>{{ trans('entities.search_for_term', ['term' => $searchTerm]) }}</a>
11 </div> 15 </div>
12 </div> 16 </div>
13 </div> 17 </div>
14 </div> 18 </div>
15 </div> 19 </div>
16 20
21 + <div class="container" ng-non-bindable id="searchSystem">
17 22
18 - <div class="container" ng-non-bindable> 23 + <div class="row">
19 24
20 - <h1>{{ trans('entities.search_results') }}</h1> 25 + <div class="col-md-6">
26 + <h1>{{ trans('entities.search_results') }}</h1>
27 + <h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
28 + @include('partials/entity-list', ['entities' => $entities])
29 + @if ($hasNextPage)
30 + <a href="{{ $nextPageLink }}" class="button">{{ trans('entities.search_more') }}</a>
31 + @endif
32 + </div>
21 33
22 - <p> 34 + <div class="col-md-5 col-md-offset-1">
23 - @if(count($pages) > 0) 35 + <h3>{{ trans('entities.search_filters') }}</h3>
24 - <a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.search_view_pages') }}</a>
25 - @endif
26 36
27 - @if(count($chapters) > 0) 37 + <form v-on:submit="updateSearch" v-cloak class="v-cloak anim fadeIn">
28 - &nbsp; &nbsp;&nbsp; 38 + <h6 class="text-muted">{{ trans('entities.search_content_type') }}</h6>
29 - <a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.search_view_chapters') }}</a> 39 + <div class="form-group">
30 - @endif 40 + <label class="inline checkbox text-page"><input type="checkbox" v-on:change="typeChange" v-model="search.type.page" value="page">{{ trans('entities.page') }}</label>
41 + <label class="inline checkbox text-chapter"><input type="checkbox" v-on:change="typeChange" v-model="search.type.chapter" value="chapter">{{ trans('entities.chapter') }}</label>
42 + <label class="inline checkbox text-book"><input type="checkbox" v-on:change="typeChange" v-model="search.type.book" value="book">{{ trans('entities.book') }}</label>
43 + </div>
31 44
32 - @if(count($books) > 0) 45 + <h6 class="text-muted">{{ trans('entities.search_exact_matches') }}</h6>
33 - &nbsp; &nbsp;&nbsp; 46 + <table cellpadding="0" cellspacing="0" border="0" class="no-style">
34 - <a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.search_view_books') }}</a> 47 + <tr v-for="(term, i) in search.exactTerms">
35 - @endif 48 + <td style="padding: 0 12px 6px 0;">
36 - </p> 49 + <input class="exact-input outline" v-on:input="exactChange" type="text" v-model="search.exactTerms[i]"></td>
50 + <td>
51 + <button type="button" class="text-neg text-button" v-on:click="removeExact(i)">
52 + <i class="zmdi zmdi-close"></i>
53 + </button>
54 + </td>
55 + </tr>
56 + <tr>
57 + <td colspan="2">
58 + <button type="button" class="text-button" v-on:click="addExact">
59 + <i class="zmdi zmdi-plus-circle-o"></i>{{ trans('common.add') }}
60 + </button>
61 + </td>
62 + </tr>
63 + </table>
64 +
65 + <h6 class="text-muted">{{ trans('entities.search_tags') }}</h6>
66 + <table cellpadding="0" cellspacing="0" border="0" class="no-style">
67 + <tr v-for="(term, i) in search.tagTerms">
68 + <td style="padding: 0 12px 6px 0;">
69 + <input class="tag-input outline" v-on:input="tagChange" type="text" v-model="search.tagTerms[i]"></td>
70 + <td>
71 + <button type="button" class="text-neg text-button" v-on:click="removeTag(i)">
72 + <i class="zmdi zmdi-close"></i>
73 + </button>
74 + </td>
75 + </tr>
76 + <tr>
77 + <td colspan="2">
78 + <button type="button" class="text-button" v-on:click="addTag">
79 + <i class="zmdi zmdi-plus-circle-o"></i>{{ trans('common.add') }}
80 + </button>
81 + </td>
82 + </tr>
83 + </table>
84 +
85 + <h6 class="text-muted">Options</h6>
86 + <label class="checkbox">
87 + <input type="checkbox" v-on:change="optionChange('viewed_by_me')"
88 + v-model="search.option.viewed_by_me" value="page">
89 + {{ trans('entities.search_viewed_by_me') }}
90 + </label>
91 + <label class="checkbox">
92 + <input type="checkbox" v-on:change="optionChange('not_viewed_by_me')"
93 + v-model="search.option.not_viewed_by_me" value="page">
94 + {{ trans('entities.search_not_viewed_by_me') }}
95 + </label>
96 + <label class="checkbox">
97 + <input type="checkbox" v-on:change="optionChange('is_restricted')"
98 + v-model="search.option.is_restricted" value="page">
99 + {{ trans('entities.search_permissions_set') }}
100 + </label>
101 + <label class="checkbox">
102 + <input type="checkbox" v-on:change="optionChange('created_by:me')"
103 + v-model="search.option['created_by:me']" value="page">
104 + {{ trans('entities.search_created_by_me') }}
105 + </label>
106 + <label class="checkbox">
107 + <input type="checkbox" v-on:change="optionChange('updated_by:me')"
108 + v-model="search.option['updated_by:me']" value="page">
109 + {{ trans('entities.search_updated_by_me') }}
110 + </label>
111 +
112 + <h6 class="text-muted">Date Options</h6>
113 + <table cellpadding="0" cellspacing="0" border="0" class="no-style form-table">
114 + <tr>
115 + <td width="200">{{ trans('entities.search_updated_after') }}</td>
116 + <td width="80">
117 + <button type="button" class="text-button" v-if="!search.dates.updated_after"
118 + v-on:click="enableDate('updated_after')">{{ trans('entities.search_set_date') }}</button>
119 +
120 + </td>
121 + </tr>
122 + <tr v-if="search.dates.updated_after">
123 + <td>
124 + <input v-if="search.dates.updated_after" class="tag-input"
125 + v-on:input="dateChange('updated_after')" type="date" v-model="search.dates.updated_after"
126 + pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
127 + </td>
128 + <td>
129 + <button v-if="search.dates.updated_after" type="button" class="text-neg text-button"
130 + v-on:click="dateRemove('updated_after')">
131 + <i class="zmdi zmdi-close"></i>
132 + </button>
133 + </td>
134 + </tr>
135 + <tr>
136 + <td>{{ trans('entities.search_updated_before') }}</td>
137 + <td>
138 + <button type="button" class="text-button" v-if="!search.dates.updated_before"
139 + v-on:click="enableDate('updated_before')">{{ trans('entities.search_set_date') }}</button>
140 +
141 + </td>
142 + </tr>
143 + <tr v-if="search.dates.updated_before">
144 + <td>
145 + <input v-if="search.dates.updated_before" class="tag-input"
146 + v-on:input="dateChange('updated_before')" type="date" v-model="search.dates.updated_before"
147 + pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
148 + </td>
149 + <td>
150 + <button v-if="search.dates.updated_before" type="button" class="text-neg text-button"
151 + v-on:click="dateRemove('updated_before')">
152 + <i class="zmdi zmdi-close"></i>
153 + </button>
154 + </td>
155 + </tr>
156 + <tr>
157 + <td>{{ trans('entities.search_created_after') }}</td>
158 + <td>
159 + <button type="button" class="text-button" v-if="!search.dates.created_after"
160 + v-on:click="enableDate('created_after')">{{ trans('entities.search_set_date') }}</button>
161 +
162 + </td>
163 + </tr>
164 + <tr v-if="search.dates.created_after">
165 + <td>
166 + <input v-if="search.dates.created_after" class="tag-input"
167 + v-on:input="dateChange('created_after')" type="date" v-model="search.dates.created_after"
168 + pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
169 + </td>
170 + <td>
171 + <button v-if="search.dates.created_after" type="button" class="text-neg text-button"
172 + v-on:click="dateRemove('created_after')">
173 + <i class="zmdi zmdi-close"></i>
174 + </button>
175 + </td>
176 + </tr>
177 + <tr>
178 + <td>{{ trans('entities.search_created_before') }}</td>
179 + <td>
180 + <button type="button" class="text-button" v-if="!search.dates.created_before"
181 + v-on:click="enableDate('created_before')">{{ trans('entities.search_set_date') }}</button>
182 +
183 + </td>
184 + </tr>
185 + <tr v-if="search.dates.created_before">
186 + <td>
187 + <input v-if="search.dates.created_before" class="tag-input"
188 + v-on:input="dateChange('created_before')" type="date" v-model="search.dates.created_before"
189 + pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
190 + </td>
191 + <td>
192 + <button v-if="search.dates.created_before" type="button" class="text-neg text-button"
193 + v-on:click="dateRemove('created_before')">
194 + <i class="zmdi zmdi-close"></i>
195 + </button>
196 + </td>
197 + </tr>
198 + </table>
199 +
200 +
201 + <button type="submit" class="button primary">{{ trans('entities.search_update') }}</button>
202 + </form>
37 203
38 - <div class="row">
39 - <div class="col-md-6">
40 - <h3><a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="no-color">{{ trans('entities.pages') }}</a></h3>
41 - @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
42 - </div>
43 - <div class="col-md-5 col-md-offset-1">
44 - @if(count($books) > 0)
45 - <h3><a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="no-color">{{ trans('entities.books') }}</a></h3>
46 - @include('partials/entity-list', ['entities' => $books])
47 - @endif
48 204
49 - @if(count($chapters) > 0)
50 - <h3><a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="no-color">{{ trans('entities.chapters') }}</a></h3>
51 - @include('partials/entity-list', ['entities' => $chapters])
52 - @endif
53 </div> 205 </div>
206 +
54 </div> 207 </div>
55 208
56 209
57 </div> 210 </div>
58 - 211 +</div>
59 212
60 @stop 213 @stop
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -123,11 +123,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -123,11 +123,9 @@ Route::group(['middleware' => 'auth'], function () {
123 Route::get('/link/{id}', 'PageController@redirectFromLink'); 123 Route::get('/link/{id}', 'PageController@redirectFromLink');
124 124
125 // Search 125 // Search
126 - Route::get('/search/all', 'SearchController@searchAll'); 126 + Route::get('/search', 'SearchController@search');
127 - Route::get('/search/pages', 'SearchController@searchPages');
128 - Route::get('/search/books', 'SearchController@searchBooks');
129 - Route::get('/search/chapters', 'SearchController@searchChapters');
130 Route::get('/search/book/{bookId}', 'SearchController@searchBook'); 127 Route::get('/search/book/{bookId}', 'SearchController@searchBook');
128 + Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
131 129
132 // Other Pages 130 // Other Pages
133 Route::get('/', 'HomeController@index'); 131 Route::get('/', 'HomeController@index');
......
...@@ -22,6 +22,12 @@ abstract class BrowserKitTest extends TestCase ...@@ -22,6 +22,12 @@ abstract class BrowserKitTest extends TestCase
22 private $admin; 22 private $admin;
23 private $editor; 23 private $editor;
24 24
25 + public function tearDown()
26 + {
27 + \DB::disconnect();
28 + parent::tearDown();
29 + }
30 +
25 /** 31 /**
26 * Creates the application. 32 * Creates the application.
27 * 33 *
......
1 <?php namespace Tests; 1 <?php namespace Tests;
2 2
3 -class EntitySearchTest extends BrowserKitTest 3 +
4 +class EntitySearchTest extends TestCase
4 { 5 {
5 6
6 public function test_page_search() 7 public function test_page_search()
...@@ -8,91 +9,57 @@ class EntitySearchTest extends BrowserKitTest ...@@ -8,91 +9,57 @@ class EntitySearchTest extends BrowserKitTest
8 $book = \BookStack\Book::all()->first(); 9 $book = \BookStack\Book::all()->first();
9 $page = $book->pages->first(); 10 $page = $book->pages->first();
10 11
11 - $this->asAdmin() 12 + $search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
12 - ->visit('/') 13 + $search->assertSee('Search Results');
13 - ->type($page->name, 'term') 14 + $search->assertSee($page->name);
14 - ->press('header-search-box-button')
15 - ->see('Search Results')
16 - ->seeInElement('.entity-list', $page->name)
17 - ->clickInElement('.entity-list', $page->name)
18 - ->seePageIs($page->getUrl());
19 } 15 }
20 16
21 public function test_invalid_page_search() 17 public function test_invalid_page_search()
22 { 18 {
23 - $this->asAdmin() 19 + $resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
24 - ->visit('/') 20 + $resp->assertSee('Search Results');
25 - ->type('<p>test</p>', 'term') 21 + $resp->assertStatus(200);
26 - ->press('header-search-box-button') 22 + $this->get('/search?term=cat+-')->assertStatus(200);
27 - ->see('Search Results')
28 - ->seeStatusCode(200);
29 } 23 }
30 24
31 - public function test_empty_search_redirects_back() 25 + public function test_empty_search_shows_search_page()
32 { 26 {
33 - $this->asAdmin() 27 + $res = $this->asEditor()->get('/search');
34 - ->visit('/') 28 + $res->assertStatus(200);
35 - ->visit('/search/all')
36 - ->seePageIs('/');
37 } 29 }
38 30
39 - public function test_book_search() 31 + public function test_searching_accents_and_small_terms()
40 { 32 {
41 - $book = \BookStack\Book::all()->first(); 33 + $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content {a2 orange dog']);
42 - $page = $book->pages->last(); 34 + $this->asEditor();
43 - $chapter = $book->chapters->last();
44 35
45 - $this->asAdmin() 36 + $accentSearch = $this->get('/search?term=' . urlencode('áéíí'));
46 - ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name)) 37 + $accentSearch->assertStatus(200)->assertSee($page->name);
47 - ->see($page->name)
48 38
49 - ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name)) 39 + $smallSearch = $this->get('/search?term=' . urlencode('{a'));
50 - ->see($chapter->name); 40 + $smallSearch->assertStatus(200)->assertSee($page->name);
51 } 41 }
52 42
53 - public function test_empty_book_search_redirects_back() 43 + public function test_book_search()
54 { 44 {
55 $book = \BookStack\Book::all()->first(); 45 $book = \BookStack\Book::all()->first();
56 - $this->asAdmin() 46 + $page = $book->pages->last();
57 - ->visit('/books') 47 + $chapter = $book->chapters->last();
58 - ->visit('/search/book/' . $book->id . '?term=')
59 - ->seePageIs('/books');
60 - }
61 -
62 -
63 - public function test_pages_search_listing()
64 - {
65 - $page = \BookStack\Page::all()->last();
66 - $this->asAdmin()->visit('/search/pages?term=' . $page->name)
67 - ->see('Page Search Results')->see('.entity-list', $page->name);
68 - }
69 48
70 - public function test_chapters_search_listing() 49 + $pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name));
71 - { 50 + $pageTestResp->assertSee($page->name);
72 - $chapter = \BookStack\Chapter::all()->last();
73 - $this->asAdmin()->visit('/search/chapters?term=' . $chapter->name)
74 - ->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
75 - }
76 51
77 - public function test_search_quote_term_preparation() 52 + $chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name));
78 - { 53 + $chapterTestResp->assertSee($chapter->name);
79 - $termString = '"192" cat "dog hat"';
80 - $repo = $this->app[\BookStack\Repos\EntityRepo::class];
81 - $preparedTerms = $repo->prepareSearchTerms($termString);
82 - $this->assertTrue($preparedTerms === ['"192"','"dog hat"', 'cat']);
83 } 54 }
84 55
85 - public function test_books_search_listing() 56 + public function test_chapter_search()
86 { 57 {
87 - $book = \BookStack\Book::all()->last(); 58 + $chapter = \BookStack\Chapter::has('pages')->first();
88 - $this->asAdmin()->visit('/search/books?term=' . $book->name) 59 + $page = $chapter->pages[0];
89 - ->see('Book Search Results')->see('.entity-list', $book->name);
90 - }
91 60
92 - public function test_searching_hypen_doesnt_break() 61 + $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));
93 - { 62 + $pageTestResp->assertSee($page->name);
94 - $this->visit('/search/all?term=cat+-')
95 - ->seeStatusCode(200);
96 } 63 }
97 64
98 public function test_tag_search() 65 public function test_tag_search()
...@@ -114,27 +81,99 @@ class EntitySearchTest extends BrowserKitTest ...@@ -114,27 +81,99 @@ class EntitySearchTest extends BrowserKitTest
114 $pageB = \BookStack\Page::all()->last(); 81 $pageB = \BookStack\Page::all()->last();
115 $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']); 82 $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
116 83
117 - $this->asAdmin()->visit('/search/all?term=%5Banimal%5D') 84 + $this->asEditor();
118 - ->seeLink($pageA->name) 85 + $tNameSearch = $this->get('/search?term=%5Banimal%5D');
119 - ->seeLink($pageB->name); 86 + $tNameSearch->assertSee($pageA->name)->assertSee($pageB->name);
120 87
121 - $this->visit('/search/all?term=%5Bcolor%5D') 88 + $tNameSearch2 = $this->get('/search?term=%5Bcolor%5D');
122 - ->seeLink($pageA->name) 89 + $tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name);
123 - ->dontSeeLink($pageB->name); 90 +
91 + $tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D');
92 + $tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name);
93 + }
94 +
95 + public function test_exact_searches()
96 + {
97 + $page = $this->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']);
124 98
125 - $this->visit('/search/all?term=%5Banimal%3Dcat%5D') 99 + $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"'));
126 - ->seeLink($pageA->name) 100 + $exactSearchA->assertStatus(200)->assertSee($page->name);
127 - ->dontSeeLink($pageB->name);
128 101
102 + $exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('"story not about an orange"'));
103 + $exactSearchB->assertStatus(200)->assertDontSee($page->name);
104 + }
105 +
106 + public function test_search_filters()
107 + {
108 + $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
109 + $this->asEditor();
110 + $editorId = $this->getEditor()->id;
111 +
112 + // Viewed filter searches
113 + $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
114 + $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name);
115 + $this->get($page->getUrl());
116 + $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name);
117 + $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name);
118 +
119 + // User filters
120 + $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
121 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
122 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorId.'}'))->assertDontSee($page->name);
123 + $page->created_by = $editorId;
124 + $page->save();
125 + $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
126 + $this->get('/search?term=' . urlencode('danzorbhsing {created_by:'.$editorId.'}'))->assertSee($page->name);
127 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
128 + $page->updated_by = $editorId;
129 + $page->save();
130 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
131 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorId.'}'))->assertSee($page->name);
132 +
133 + // Content filters
134 + $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
135 + $this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name);
136 + $this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name);
137 + $this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name);
138 +
139 + // Restricted filter
140 + $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
141 + $page->restricted = true;
142 + $page->save();
143 + $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
144 +
145 + // Date filters
146 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name);
147 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name);
148 + $page->updated_at = '2037-02-01';
149 + $page->save();
150 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name);
151 + $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name);
152 +
153 + $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name);
154 + $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name);
155 + $page->created_at = '2037-02-01';
156 + $page->save();
157 + $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name);
158 + $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
129 } 159 }
130 160
131 public function test_ajax_entity_search() 161 public function test_ajax_entity_search()
132 { 162 {
133 $page = \BookStack\Page::all()->last(); 163 $page = \BookStack\Page::all()->last();
134 $notVisitedPage = \BookStack\Page::first(); 164 $notVisitedPage = \BookStack\Page::first();
135 - $this->visit($page->getUrl()); 165 +
136 - $this->asAdmin()->visit('/ajax/search/entities?term=' . $page->name)->see('.entity-list', $page->name); 166 + // Visit the page to make popular
137 - $this->asAdmin()->visit('/ajax/search/entities?types=book&term=' . $page->name)->dontSee('.entity-list', $page->name); 167 + $this->asEditor()->get($page->getUrl());
138 - $this->asAdmin()->visit('/ajax/search/entities')->see('.entity-list', $page->name)->dontSee($notVisitedPage->name); 168 +
169 + $normalSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
170 + $normalSearch->assertSee($page->name);
171 +
172 + $bookSearch = $this->get('/ajax/search/entities?types=book&term=' . urlencode($page->name));
173 + $bookSearch->assertDontSee($page->name);
174 +
175 + $defaultListTest = $this->get('/ajax/search/entities');
176 + $defaultListTest->assertSee($page->name);
177 + $defaultListTest->assertDontSee($notVisitedPage->name);
139 } 178 }
140 } 179 }
......
1 <?php namespace Tests; 1 <?php namespace Tests;
2 2
3 +use BookStack\Book;
4 +use BookStack\Chapter;
5 +use BookStack\Page;
6 +use BookStack\Repos\EntityRepo;
7 +use BookStack\Repos\UserRepo;
8 +
3 class EntityTest extends BrowserKitTest 9 class EntityTest extends BrowserKitTest
4 { 10 {
5 11
...@@ -18,7 +24,7 @@ class EntityTest extends BrowserKitTest ...@@ -18,7 +24,7 @@ class EntityTest extends BrowserKitTest
18 $this->bookDelete($book); 24 $this->bookDelete($book);
19 } 25 }
20 26
21 - public function bookDelete(\BookStack\Book $book) 27 + public function bookDelete(Book $book)
22 { 28 {
23 $this->asAdmin() 29 $this->asAdmin()
24 ->visit($book->getUrl()) 30 ->visit($book->getUrl())
...@@ -32,7 +38,7 @@ class EntityTest extends BrowserKitTest ...@@ -32,7 +38,7 @@ class EntityTest extends BrowserKitTest
32 ->notSeeInDatabase('books', ['id' => $book->id]); 38 ->notSeeInDatabase('books', ['id' => $book->id]);
33 } 39 }
34 40
35 - public function bookUpdate(\BookStack\Book $book) 41 + public function bookUpdate(Book $book)
36 { 42 {
37 $newName = $book->name . ' Updated'; 43 $newName = $book->name . ' Updated';
38 $this->asAdmin() 44 $this->asAdmin()
...@@ -46,12 +52,12 @@ class EntityTest extends BrowserKitTest ...@@ -46,12 +52,12 @@ class EntityTest extends BrowserKitTest
46 ->seePageIs($book->getUrl() . '-updated') 52 ->seePageIs($book->getUrl() . '-updated')
47 ->see($newName); 53 ->see($newName);
48 54
49 - return \BookStack\Book::find($book->id); 55 + return Book::find($book->id);
50 } 56 }
51 57
52 public function test_book_sort_page_shows() 58 public function test_book_sort_page_shows()
53 { 59 {
54 - $books = \BookStack\Book::all(); 60 + $books = Book::all();
55 $bookToSort = $books[0]; 61 $bookToSort = $books[0];
56 $this->asAdmin() 62 $this->asAdmin()
57 ->visit($bookToSort->getUrl()) 63 ->visit($bookToSort->getUrl())
...@@ -65,7 +71,7 @@ class EntityTest extends BrowserKitTest ...@@ -65,7 +71,7 @@ class EntityTest extends BrowserKitTest
65 71
66 public function test_book_sort_item_returns_book_content() 72 public function test_book_sort_item_returns_book_content()
67 { 73 {
68 - $books = \BookStack\Book::all(); 74 + $books = Book::all();
69 $bookToSort = $books[0]; 75 $bookToSort = $books[0];
70 $firstPage = $bookToSort->pages[0]; 76 $firstPage = $bookToSort->pages[0];
71 $firstChapter = $bookToSort->chapters[0]; 77 $firstChapter = $bookToSort->chapters[0];
...@@ -79,7 +85,7 @@ class EntityTest extends BrowserKitTest ...@@ -79,7 +85,7 @@ class EntityTest extends BrowserKitTest
79 85
80 public function pageCreation($chapter) 86 public function pageCreation($chapter)
81 { 87 {
82 - $page = factory(\BookStack\Page::class)->make([ 88 + $page = factory(Page::class)->make([
83 'name' => 'My First Page' 89 'name' => 'My First Page'
84 ]); 90 ]);
85 91
...@@ -88,7 +94,7 @@ class EntityTest extends BrowserKitTest ...@@ -88,7 +94,7 @@ class EntityTest extends BrowserKitTest
88 ->visit($chapter->getUrl()) 94 ->visit($chapter->getUrl())
89 ->click('New Page'); 95 ->click('New Page');
90 96
91 - $draftPage = \BookStack\Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first(); 97 + $draftPage = Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
92 98
93 $this->seePageIs($draftPage->getUrl()) 99 $this->seePageIs($draftPage->getUrl())
94 // Fill out form 100 // Fill out form
...@@ -99,13 +105,13 @@ class EntityTest extends BrowserKitTest ...@@ -99,13 +105,13 @@ class EntityTest extends BrowserKitTest
99 ->seePageIs($chapter->book->getUrl() . '/page/my-first-page') 105 ->seePageIs($chapter->book->getUrl() . '/page/my-first-page')
100 ->see($page->name); 106 ->see($page->name);
101 107
102 - $page = \BookStack\Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first(); 108 + $page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first();
103 return $page; 109 return $page;
104 } 110 }
105 111
106 - public function chapterCreation(\BookStack\Book $book) 112 + public function chapterCreation(Book $book)
107 { 113 {
108 - $chapter = factory(\BookStack\Chapter::class)->make([ 114 + $chapter = factory(Chapter::class)->make([
109 'name' => 'My First Chapter' 115 'name' => 'My First Chapter'
110 ]); 116 ]);
111 117
...@@ -122,13 +128,13 @@ class EntityTest extends BrowserKitTest ...@@ -122,13 +128,13 @@ class EntityTest extends BrowserKitTest
122 ->seePageIs($book->getUrl() . '/chapter/my-first-chapter') 128 ->seePageIs($book->getUrl() . '/chapter/my-first-chapter')
123 ->see($chapter->name)->see($chapter->description); 129 ->see($chapter->name)->see($chapter->description);
124 130
125 - $chapter = \BookStack\Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first(); 131 + $chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first();
126 return $chapter; 132 return $chapter;
127 } 133 }
128 134
129 public function bookCreation() 135 public function bookCreation()
130 { 136 {
131 - $book = factory(\BookStack\Book::class)->make([ 137 + $book = factory(Book::class)->make([
132 'name' => 'My First Book' 138 'name' => 'My First Book'
133 ]); 139 ]);
134 $this->asAdmin() 140 $this->asAdmin()
...@@ -154,7 +160,7 @@ class EntityTest extends BrowserKitTest ...@@ -154,7 +160,7 @@ class EntityTest extends BrowserKitTest
154 $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/'; 160 $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/';
155 $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); 161 $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
156 162
157 - $book = \BookStack\Book::where('slug', '=', 'my-first-book')->first(); 163 + $book = Book::where('slug', '=', 'my-first-book')->first();
158 return $book; 164 return $book;
159 } 165 }
160 166
...@@ -165,8 +171,8 @@ class EntityTest extends BrowserKitTest ...@@ -165,8 +171,8 @@ class EntityTest extends BrowserKitTest
165 $updater = $this->getEditor(); 171 $updater = $this->getEditor();
166 $entities = $this->createEntityChainBelongingToUser($creator, $updater); 172 $entities = $this->createEntityChainBelongingToUser($creator, $updater);
167 $this->actingAs($creator); 173 $this->actingAs($creator);
168 - app('BookStack\Repos\UserRepo')->destroy($creator); 174 + app(UserRepo::class)->destroy($creator);
169 - app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']); 175 + app(EntityRepo::class)->savePageRevision($entities['page']);
170 176
171 $this->checkEntitiesViewable($entities); 177 $this->checkEntitiesViewable($entities);
172 } 178 }
...@@ -178,8 +184,8 @@ class EntityTest extends BrowserKitTest ...@@ -178,8 +184,8 @@ class EntityTest extends BrowserKitTest
178 $updater = $this->getEditor(); 184 $updater = $this->getEditor();
179 $entities = $this->createEntityChainBelongingToUser($creator, $updater); 185 $entities = $this->createEntityChainBelongingToUser($creator, $updater);
180 $this->actingAs($updater); 186 $this->actingAs($updater);
181 - app('BookStack\Repos\UserRepo')->destroy($updater); 187 + app(UserRepo::class)->destroy($updater);
182 - app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']); 188 + app(EntityRepo::class)->savePageRevision($entities['page']);
183 189
184 $this->checkEntitiesViewable($entities); 190 $this->checkEntitiesViewable($entities);
185 } 191 }
...@@ -216,7 +222,7 @@ class EntityTest extends BrowserKitTest ...@@ -216,7 +222,7 @@ class EntityTest extends BrowserKitTest
216 222
217 public function test_old_page_slugs_redirect_to_new_pages() 223 public function test_old_page_slugs_redirect_to_new_pages()
218 { 224 {
219 - $page = \BookStack\Page::first(); 225 + $page = Page::first();
220 $pageUrl = $page->getUrl(); 226 $pageUrl = $page->getUrl();
221 $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page'; 227 $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
222 // Need to save twice since revisions are not generated in seeder. 228 // Need to save twice since revisions are not generated in seeder.
...@@ -225,7 +231,7 @@ class EntityTest extends BrowserKitTest ...@@ -225,7 +231,7 @@ class EntityTest extends BrowserKitTest
225 ->type('super test', '#name') 231 ->type('super test', '#name')
226 ->press('Save Page'); 232 ->press('Save Page');
227 233
228 - $page = \BookStack\Page::first(); 234 + $page = Page::first();
229 $pageUrl = $page->getUrl(); 235 $pageUrl = $page->getUrl();
230 236
231 // Second Save 237 // Second Save
...@@ -242,7 +248,7 @@ class EntityTest extends BrowserKitTest ...@@ -242,7 +248,7 @@ class EntityTest extends BrowserKitTest
242 248
243 public function test_recently_updated_pages_on_home() 249 public function test_recently_updated_pages_on_home()
244 { 250 {
245 - $page = \BookStack\Page::orderBy('updated_at', 'asc')->first(); 251 + $page = Page::orderBy('updated_at', 'asc')->first();
246 $this->asAdmin()->visit('/') 252 $this->asAdmin()->visit('/')
247 ->dontSeeInElement('#recently-updated-pages', $page->name); 253 ->dontSeeInElement('#recently-updated-pages', $page->name);
248 $this->visit($page->getUrl() . '/edit') 254 $this->visit($page->getUrl() . '/edit')
......
1 +<?php namespace Entity;
2 +
3 +
4 +use BookStack\Page;
5 +use Tests\TestCase;
6 +
7 +class PageRevisionTest extends TestCase
8 +{
9 +
10 + public function test_page_revision_count_increments_on_update()
11 + {
12 + $page = Page::first();
13 + $startCount = $page->revision_count;
14 +
15 + $resp = $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
16 + $resp->assertStatus(302);
17 +
18 + $this->assertTrue(Page::find($page->id)->revision_count === $startCount+1);
19 + }
20 +
21 + public function test_revision_count_shown_in_page_meta()
22 + {
23 + $page = Page::first();
24 + $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
25 + $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
26 + $page = Page::find($page->id);
27 +
28 + $pageView = $this->get($page->getUrl());
29 + $pageView->assertSee('Revision #' . $page->revision_count);
30 + }
31 +
32 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -522,4 +522,21 @@ class RestrictionsTest extends BrowserKitTest ...@@ -522,4 +522,21 @@ class RestrictionsTest extends BrowserKitTest
522 ->see('Delete Chapter'); 522 ->see('Delete Chapter');
523 } 523 }
524 524
525 + public function test_page_visible_if_has_permissions_when_book_not_visible()
526 + {
527 + $book = \BookStack\Book::first();
528 + $bookChapter = $book->chapters->first();
529 + $bookPage = $bookChapter->pages->first();
530 +
531 + $this->setEntityRestrictions($book, []);
532 + $this->setEntityRestrictions($bookPage, ['view']);
533 +
534 + $this->actingAs($this->viewer);
535 + $this->get($bookPage->getUrl());
536 + $this->assertResponseOk();
537 + $this->see($bookPage->name);
538 + $this->dontSee(substr($book->name, 0, 15));
539 + $this->dontSee(substr($bookChapter->name, 0, 15));
540 + }
541 +
525 } 542 }
......
...@@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase ...@@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase
76 public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) { 76 public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
77 return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book); 77 return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
78 } 78 }
79 +
80 + /**
81 + * Create and return a new test page
82 + * @param array $input
83 + * @return Chapter
84 + */
85 + public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
86 + $book = Book::first();
87 + $entityRepo = $this->app[EntityRepo::class];
88 + $draftPage = $entityRepo->getDraftPage($book);
89 + return $entityRepo->publishPageDraft($draftPage, $input);
90 + }
79 } 91 }
...\ No newline at end of file ...\ No newline at end of file
......
1 -v0.15.3 1 +v0.16
......