Dan Brown

Merge branch 'master' into release

Showing 44 changed files with 666 additions and 257 deletions
1 +### For Feature Requests
2 +Desired Feature:
3 +
4 +### For Bug Reports
5 +PHP Version:
6 +
7 +MySQL Version:
8 +
9 +Expected Behavior:
10 +
11 +Actual Behavior:
...@@ -6,8 +6,6 @@ php: ...@@ -6,8 +6,6 @@ php:
6 6
7 cache: 7 cache:
8 directories: 8 directories:
9 - - vendor
10 - - node_modules
11 - $HOME/.composer/cache 9 - $HOME/.composer/cache
12 10
13 addons: 11 addons:
...@@ -17,19 +15,17 @@ addons: ...@@ -17,19 +15,17 @@ addons:
17 - mysql-client-core-5.6 15 - mysql-client-core-5.6
18 - mysql-client-5.6 16 - mysql-client-5.6
19 17
20 -before_install:
21 - - npm install -g npm@latest
22 -
23 before_script: 18 before_script:
24 - mysql -u root -e 'create database `bookstack-test`;' 19 - mysql -u root -e 'create database `bookstack-test`;'
25 - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN 20 - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
26 - phpenv config-rm xdebug.ini 21 - phpenv config-rm xdebug.ini
27 - composer self-update 22 - composer self-update
23 + - composer dump-autoload --no-interaction
28 - composer install --prefer-dist --no-interaction 24 - composer install --prefer-dist --no-interaction
29 - - npm install 25 + - php artisan clear-compiled -n
30 - - ./node_modules/.bin/gulp 26 + - php artisan optimize -n
31 - php artisan migrate --force -n --database=mysql_testing 27 - php artisan migrate --force -n --database=mysql_testing
32 - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing 28 - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
33 29
34 script: 30 script:
35 - - vendor/bin/phpunit
...\ No newline at end of file ...\ No newline at end of file
31 + - phpunit
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -167,7 +167,8 @@ class Entity extends Ownable ...@@ -167,7 +167,8 @@ class Entity extends Ownable
167 foreach ($terms as $key => $term) { 167 foreach ($terms as $key => $term) {
168 $term = htmlentities($term, ENT_QUOTES); 168 $term = htmlentities($term, ENT_QUOTES);
169 $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); 169 $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
170 - if (preg_match('/\s/', $term)) { 170 + if (preg_match('/&quot;.*?&quot;/', $term)) {
171 + $term = str_replace('&quot;', '', $term);
171 $exactTerms[] = '%' . $term . '%'; 172 $exactTerms[] = '%' . $term . '%';
172 $term = '"' . $term . '"'; 173 $term = '"' . $term . '"';
173 } else { 174 } else {
...@@ -206,5 +207,5 @@ class Entity extends Ownable ...@@ -206,5 +207,5 @@ class Entity extends Ownable
206 207
207 return $search->orderBy($orderBy, 'desc'); 208 return $search->orderBy($orderBy, 'desc');
208 } 209 }
209 - 210 +
210 } 211 }
......
...@@ -47,19 +47,44 @@ class Handler extends ExceptionHandler ...@@ -47,19 +47,44 @@ class Handler extends ExceptionHandler
47 { 47 {
48 // Handle notify exceptions which will redirect to the 48 // Handle notify exceptions which will redirect to the
49 // specified location then show a notification message. 49 // specified location then show a notification message.
50 - if ($e instanceof NotifyException) { 50 + if ($this->isExceptionType($e, NotifyException::class)) {
51 - session()->flash('error', $e->message); 51 + session()->flash('error', $this->getOriginalMessage($e));
52 return redirect($e->redirectLocation); 52 return redirect($e->redirectLocation);
53 } 53 }
54 54
55 // Handle pretty exceptions which will show a friendly application-fitting page 55 // Handle pretty exceptions which will show a friendly application-fitting page
56 // Which will include the basic message to point the user roughly to the cause. 56 // Which will include the basic message to point the user roughly to the cause.
57 - if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) { 57 + if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
58 - $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); 58 + $message = $this->getOriginalMessage($e);
59 $code = ($e->getCode() === 0) ? 500 : $e->getCode(); 59 $code = ($e->getCode() === 0) ? 500 : $e->getCode();
60 return response()->view('errors/' . $code, ['message' => $message], $code); 60 return response()->view('errors/' . $code, ['message' => $message], $code);
61 } 61 }
62 62
63 return parent::render($request, $e); 63 return parent::render($request, $e);
64 } 64 }
65 +
66 + /**
67 + * Check the exception chain to compare against the original exception type.
68 + * @param Exception $e
69 + * @param $type
70 + * @return bool
71 + */
72 + protected function isExceptionType(Exception $e, $type) {
73 + do {
74 + if (is_a($e, $type)) return true;
75 + } while ($e = $e->getPrevious());
76 + return false;
77 + }
78 +
79 + /**
80 + * Get original exception message.
81 + * @param Exception $e
82 + * @return string
83 + */
84 + protected function getOriginalMessage(Exception $e) {
85 + do {
86 + $message = $e->getMessage();
87 + } while ($e = $e->getPrevious());
88 + return $message;
89 + }
65 } 90 }
......
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 -use Exception;
4 -
5 -class PrettyException extends Exception {}
...\ No newline at end of file ...\ No newline at end of file
3 +class PrettyException extends \Exception {}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
3 use Activity; 3 use Activity;
4 use BookStack\Repos\UserRepo; 4 use BookStack\Repos\UserRepo;
5 use Illuminate\Http\Request; 5 use Illuminate\Http\Request;
6 -use Illuminate\Support\Facades\Auth;
7 use BookStack\Http\Requests; 6 use BookStack\Http\Requests;
8 use BookStack\Repos\BookRepo; 7 use BookStack\Repos\BookRepo;
9 use BookStack\Repos\ChapterRepo; 8 use BookStack\Repos\ChapterRepo;
...@@ -180,21 +179,31 @@ class BookController extends Controller ...@@ -180,21 +179,31 @@ class BookController extends Controller
180 return redirect($book->getUrl()); 179 return redirect($book->getUrl());
181 } 180 }
182 181
183 - $sortedBooks = [];
184 // Sort pages and chapters 182 // Sort pages and chapters
183 + $sortedBooks = [];
184 + $updatedModels = collect();
185 $sortMap = json_decode($request->get('sort-tree')); 185 $sortMap = json_decode($request->get('sort-tree'));
186 $defaultBookId = $book->id; 186 $defaultBookId = $book->id;
187 - foreach ($sortMap as $index => $bookChild) { 187 +
188 - $id = $bookChild->id; 188 + // Loop through contents of provided map and update entities accordingly
189 + foreach ($sortMap as $bookChild) {
190 + $priority = $bookChild->sort;
191 + $id = intval($bookChild->id);
189 $isPage = $bookChild->type == 'page'; 192 $isPage = $bookChild->type == 'page';
190 - $bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId; 193 + $bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
194 + $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
191 $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id); 195 $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
192 - $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model); 196 +
193 - $model->priority = $index; 197 + // Update models only if there's a change in parent chain or ordering.
194 - if ($isPage) { 198 + if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
195 - $model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter; 199 + $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
200 + $model->priority = $priority;
201 + if ($isPage) $model->chapter_id = $chapterId;
202 + $model->save();
203 + $updatedModels->push($model);
196 } 204 }
197 - $model->save(); 205 +
206 + // Store involved books to be sorted later
198 if (!in_array($bookId, $sortedBooks)) { 207 if (!in_array($bookId, $sortedBooks)) {
199 $sortedBooks[] = $bookId; 208 $sortedBooks[] = $bookId;
200 } 209 }
...@@ -203,10 +212,12 @@ class BookController extends Controller ...@@ -203,10 +212,12 @@ class BookController extends Controller
203 // Add activity for books 212 // Add activity for books
204 foreach ($sortedBooks as $bookId) { 213 foreach ($sortedBooks as $bookId) {
205 $updatedBook = $this->bookRepo->getById($bookId); 214 $updatedBook = $this->bookRepo->getById($bookId);
206 - $this->bookRepo->updateBookPermissions($updatedBook);
207 Activity::add($updatedBook, 'book_sort', $updatedBook->id); 215 Activity::add($updatedBook, 'book_sort', $updatedBook->id);
208 } 216 }
209 217
218 + // Update permissions on changed models
219 + $this->bookRepo->buildJointPermissions($updatedModels);
220 +
210 return redirect($book->getUrl()); 221 return redirect($book->getUrl());
211 } 222 }
212 223
......
...@@ -204,7 +204,7 @@ class ChapterController extends Controller ...@@ -204,7 +204,7 @@ class ChapterController extends Controller
204 return redirect()->back(); 204 return redirect()->back();
205 } 205 }
206 206
207 - $this->chapterRepo->changeBook($parent->id, $chapter); 207 + $this->chapterRepo->changeBook($parent->id, $chapter, true);
208 Activity::add($chapter, 'chapter_move', $chapter->book->id); 208 Activity::add($chapter, 'chapter_move', $chapter->book->id);
209 session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name)); 209 session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
210 210
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 3
4 class PageRevision extends Model 4 class PageRevision extends Model
5 { 5 {
6 - protected $fillable = ['name', 'html', 'text', 'markdown']; 6 + protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
7 7
8 /** 8 /**
9 * Get the user that created the page revision 9 * Get the user that created the page revision
......
1 -<?php 1 +<?php namespace BookStack\Providers;
2 2
3 -namespace BookStack\Providers;
4 -
5 -use Illuminate\Support\Facades\Auth;
6 use Illuminate\Support\ServiceProvider; 3 use Illuminate\Support\ServiceProvider;
7 -use BookStack\User;
8 4
9 class AppServiceProvider extends ServiceProvider 5 class AppServiceProvider extends ServiceProvider
10 { 6 {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 use Alpha\B; 3 use Alpha\B;
4 use BookStack\Exceptions\NotFoundException; 4 use BookStack\Exceptions\NotFoundException;
5 +use Illuminate\Database\Eloquent\Collection;
5 use Illuminate\Support\Str; 6 use Illuminate\Support\Str;
6 use BookStack\Book; 7 use BookStack\Book;
7 use Views; 8 use Views;
...@@ -174,15 +175,6 @@ class BookRepo extends EntityRepo ...@@ -174,15 +175,6 @@ class BookRepo extends EntityRepo
174 } 175 }
175 176
176 /** 177 /**
177 - * Alias method to update the book jointPermissions in the PermissionService.
178 - * @param Book $book
179 - */
180 - public function updateBookPermissions(Book $book)
181 - {
182 - $this->permissionService->buildJointPermissionsForEntity($book);
183 - }
184 -
185 - /**
186 * Get the next child element priority. 178 * Get the next child element priority.
187 * @param Book $book 179 * @param Book $book
188 * @return int 180 * @return int
......
...@@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo ...@@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo
195 195
196 /** 196 /**
197 * Changes the book relation of this chapter. 197 * Changes the book relation of this chapter.
198 - * @param $bookId 198 + * @param $bookId
199 * @param Chapter $chapter 199 * @param Chapter $chapter
200 + * @param bool $rebuildPermissions
200 * @return Chapter 201 * @return Chapter
201 */ 202 */
202 - public function changeBook($bookId, Chapter $chapter) 203 + public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
203 { 204 {
204 $chapter->book_id = $bookId; 205 $chapter->book_id = $bookId;
205 // Update related activity 206 // Update related activity
...@@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo ...@@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo
213 foreach ($chapter->pages as $page) { 214 foreach ($chapter->pages as $page) {
214 $this->pageRepo->changeBook($bookId, $page); 215 $this->pageRepo->changeBook($bookId, $page);
215 } 216 }
216 - // Update permissions 217 +
217 - $chapter->load('book'); 218 + // Update permissions if applicable
218 - $this->permissionService->buildJointPermissionsForEntity($chapter->book); 219 + if ($rebuildPermissions) {
220 + $chapter->load('book');
221 + $this->permissionService->buildJointPermissionsForEntity($chapter->book);
222 + }
219 223
220 return $chapter; 224 return $chapter;
221 } 225 }
......
...@@ -6,6 +6,7 @@ use BookStack\Entity; ...@@ -6,6 +6,7 @@ use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 use BookStack\Services\PermissionService; 7 use BookStack\Services\PermissionService;
8 use BookStack\User; 8 use BookStack\User;
9 +use Illuminate\Support\Collection;
9 use Illuminate\Support\Facades\Log; 10 use Illuminate\Support\Facades\Log;
10 11
11 class EntityRepo 12 class EntityRepo
...@@ -168,15 +169,16 @@ class EntityRepo ...@@ -168,15 +169,16 @@ class EntityRepo
168 * @param $termString 169 * @param $termString
169 * @return array 170 * @return array
170 */ 171 */
171 - protected function prepareSearchTerms($termString) 172 + public function prepareSearchTerms($termString)
172 { 173 {
173 $termString = $this->cleanSearchTermString($termString); 174 $termString = $this->cleanSearchTermString($termString);
174 - preg_match_all('/"(.*?)"/', $termString, $matches); 175 + preg_match_all('/(".*?")/', $termString, $matches);
176 + $terms = [];
175 if (count($matches[1]) > 0) { 177 if (count($matches[1]) > 0) {
176 - $terms = $matches[1]; 178 + foreach ($matches[1] as $match) {
179 + $terms[] = $match;
180 + }
177 $termString = trim(preg_replace('/"(.*?)"/', '', $termString)); 181 $termString = trim(preg_replace('/"(.*?)"/', '', $termString));
178 - } else {
179 - $terms = [];
180 } 182 }
181 if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString)); 183 if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
182 return $terms; 184 return $terms;
...@@ -259,6 +261,15 @@ class EntityRepo ...@@ -259,6 +261,15 @@ class EntityRepo
259 return $query; 261 return $query;
260 } 262 }
261 263
264 + /**
265 + * Alias method to update the book jointPermissions in the PermissionService.
266 + * @param Collection $collection collection on entities
267 + */
268 + public function buildJointPermissions(Collection $collection)
269 + {
270 + $this->permissionService->buildJointPermissionsForEntities($collection);
271 + }
272 +
262 } 273 }
263 274
264 275
......
...@@ -157,6 +157,8 @@ class PageRepo extends EntityRepo ...@@ -157,6 +157,8 @@ class PageRepo extends EntityRepo
157 $draftPage->draft = false; 157 $draftPage->draft = false;
158 158
159 $draftPage->save(); 159 $draftPage->save();
160 + $this->saveRevision($draftPage, 'Initial Publish');
161 +
160 return $draftPage; 162 return $draftPage;
161 } 163 }
162 164
...@@ -308,10 +310,9 @@ class PageRepo extends EntityRepo ...@@ -308,10 +310,9 @@ class PageRepo extends EntityRepo
308 */ 310 */
309 public function updatePage(Page $page, $book_id, $input) 311 public function updatePage(Page $page, $book_id, $input)
310 { 312 {
311 - // Save a revision before updating 313 + // Hold the old details to compare later
312 - if ($page->html !== $input['html'] || $page->name !== $input['name']) { 314 + $oldHtml = $page->html;
313 - $this->saveRevision($page); 315 + $oldName = $page->name;
314 - }
315 316
316 // Prevent slug being updated if no name change 317 // Prevent slug being updated if no name change
317 if ($page->name !== $input['name']) { 318 if ($page->name !== $input['name']) {
...@@ -335,6 +336,11 @@ class PageRepo extends EntityRepo ...@@ -335,6 +336,11 @@ class PageRepo extends EntityRepo
335 // Remove all update drafts for this user & page. 336 // Remove all update drafts for this user & page.
336 $this->userUpdateDraftsQuery($page, $userId)->delete(); 337 $this->userUpdateDraftsQuery($page, $userId)->delete();
337 338
339 + // Save a revision after updating
340 + if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
341 + $this->saveRevision($page, $input['summary']);
342 + }
343 +
338 return $page; 344 return $page;
339 } 345 }
340 346
...@@ -360,9 +366,10 @@ class PageRepo extends EntityRepo ...@@ -360,9 +366,10 @@ class PageRepo extends EntityRepo
360 /** 366 /**
361 * Saves a page revision into the system. 367 * Saves a page revision into the system.
362 * @param Page $page 368 * @param Page $page
369 + * @param null|string $summary
363 * @return $this 370 * @return $this
364 */ 371 */
365 - public function saveRevision(Page $page) 372 + public function saveRevision(Page $page, $summary = null)
366 { 373 {
367 $revision = $this->pageRevision->fill($page->toArray()); 374 $revision = $this->pageRevision->fill($page->toArray());
368 if (setting('app-editor') !== 'markdown') $revision->markdown = ''; 375 if (setting('app-editor') !== 'markdown') $revision->markdown = '';
...@@ -372,6 +379,7 @@ class PageRepo extends EntityRepo ...@@ -372,6 +379,7 @@ class PageRepo extends EntityRepo
372 $revision->created_by = auth()->user()->id; 379 $revision->created_by = auth()->user()->id;
373 $revision->created_at = $page->updated_at; 380 $revision->created_at = $page->updated_at;
374 $revision->type = 'version'; 381 $revision->type = 'version';
382 + $revision->summary = $summary;
375 $revision->save(); 383 $revision->save();
376 // Clear old revisions 384 // Clear old revisions
377 if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { 385 if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
......
...@@ -48,11 +48,13 @@ class ExportService ...@@ -48,11 +48,13 @@ class ExportService
48 foreach ($imageTagsOutput[0] as $index => $imgMatch) { 48 foreach ($imageTagsOutput[0] as $index => $imgMatch) {
49 $oldImgString = $imgMatch; 49 $oldImgString = $imgMatch;
50 $srcString = $imageTagsOutput[2][$index]; 50 $srcString = $imageTagsOutput[2][$index];
51 - if (strpos(trim($srcString), 'http') !== 0) { 51 + $isLocal = strpos(trim($srcString), 'http') !== 0;
52 - $pathString = public_path($srcString); 52 + if ($isLocal) {
53 + $pathString = public_path(trim($srcString, '/'));
53 } else { 54 } else {
54 $pathString = $srcString; 55 $pathString = $srcString;
55 } 56 }
57 + if ($isLocal && !file_exists($pathString)) continue;
56 $imageContent = file_get_contents($pathString); 58 $imageContent = file_get_contents($pathString);
57 $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); 59 $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
58 $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); 60 $newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
......
...@@ -95,6 +95,7 @@ class ImageService ...@@ -95,6 +95,7 @@ class ImageService
95 95
96 try { 96 try {
97 $storage->put($fullPath, $imageData); 97 $storage->put($fullPath, $imageData);
98 + $storage->setVisibility($fullPath, 'public');
98 } catch (Exception $e) { 99 } catch (Exception $e) {
99 throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); 100 throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
100 } 101 }
...@@ -167,6 +168,7 @@ class ImageService ...@@ -167,6 +168,7 @@ class ImageService
167 168
168 $thumbData = (string)$thumb->encode(); 169 $thumbData = (string)$thumb->encode();
169 $storage->put($thumbFilePath, $thumbData); 170 $storage->put($thumbFilePath, $thumbData);
171 + $storage->setVisibility($thumbFilePath, 'public');
170 $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); 172 $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
171 173
172 return $this->getPublicUrl($thumbFilePath); 174 return $this->getPublicUrl($thumbFilePath);
...@@ -257,9 +259,15 @@ class ImageService ...@@ -257,9 +259,15 @@ class ImageService
257 $storageUrl = config('filesystems.url'); 259 $storageUrl = config('filesystems.url');
258 260
259 // Get the standard public s3 url if s3 is set as storage type 261 // Get the standard public s3 url if s3 is set as storage type
262 + // Uses the nice, short URL if bucket name has no periods in otherwise the longer
263 + // region-based url will be used to prevent http issues.
260 if ($storageUrl == false && config('filesystems.default') === 's3') { 264 if ($storageUrl == false && config('filesystems.default') === 's3') {
261 $storageDetails = config('filesystems.disks.s3'); 265 $storageDetails = config('filesystems.disks.s3');
262 - $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; 266 + if (strpos($storageDetails['bucket'], '.') === false) {
267 + $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
268 + } else {
269 + $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
270 + }
263 } 271 }
264 272
265 $this->storageUrl = $storageUrl; 273 $this->storageUrl = $storageUrl;
...@@ -269,4 +277,4 @@ class ImageService ...@@ -269,4 +277,4 @@ class ImageService
269 } 277 }
270 278
271 279
272 -}
...\ No newline at end of file ...\ No newline at end of file
280 +}
......
...@@ -8,7 +8,7 @@ use BookStack\Ownable; ...@@ -8,7 +8,7 @@ use BookStack\Ownable;
8 use BookStack\Page; 8 use BookStack\Page;
9 use BookStack\Role; 9 use BookStack\Role;
10 use BookStack\User; 10 use BookStack\User;
11 -use Illuminate\Database\Eloquent\Collection; 11 +use Illuminate\Support\Collection;
12 12
13 class PermissionService 13 class PermissionService
14 { 14 {
...@@ -25,6 +25,8 @@ class PermissionService ...@@ -25,6 +25,8 @@ class PermissionService
25 protected $jointPermission; 25 protected $jointPermission;
26 protected $role; 26 protected $role;
27 27
28 + protected $entityCache;
29 +
28 /** 30 /**
29 * PermissionService constructor. 31 * PermissionService constructor.
30 * @param JointPermission $jointPermission 32 * @param JointPermission $jointPermission
...@@ -49,6 +51,57 @@ class PermissionService ...@@ -49,6 +51,57 @@ class PermissionService
49 } 51 }
50 52
51 /** 53 /**
54 + * Prepare the local entity cache and ensure it's empty
55 + */
56 + protected function readyEntityCache()
57 + {
58 + $this->entityCache = [
59 + 'books' => collect(),
60 + 'chapters' => collect()
61 + ];
62 + }
63 +
64 + /**
65 + * Get a book via ID, Checks local cache
66 + * @param $bookId
67 + * @return Book
68 + */
69 + protected function getBook($bookId)
70 + {
71 + if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
72 + return $this->entityCache['books']->get($bookId);
73 + }
74 +
75 + $book = $this->book->find($bookId);
76 + if ($book === null) $book = false;
77 + if (isset($this->entityCache['books'])) {
78 + $this->entityCache['books']->put($bookId, $book);
79 + }
80 +
81 + return $book;
82 + }
83 +
84 + /**
85 + * Get a chapter via ID, Checks local cache
86 + * @param $chapterId
87 + * @return Book
88 + */
89 + protected function getChapter($chapterId)
90 + {
91 + if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
92 + return $this->entityCache['chapters']->get($chapterId);
93 + }
94 +
95 + $chapter = $this->chapter->find($chapterId);
96 + if ($chapter === null) $chapter = false;
97 + if (isset($this->entityCache['chapters'])) {
98 + $this->entityCache['chapters']->put($chapterId, $chapter);
99 + }
100 +
101 + return $chapter;
102 + }
103 +
104 + /**
52 * Get the roles for the current user; 105 * Get the roles for the current user;
53 * @return array|bool 106 * @return array|bool
54 */ 107 */
...@@ -76,6 +129,7 @@ class PermissionService ...@@ -76,6 +129,7 @@ class PermissionService
76 public function buildJointPermissions() 129 public function buildJointPermissions()
77 { 130 {
78 $this->jointPermission->truncate(); 131 $this->jointPermission->truncate();
132 + $this->readyEntityCache();
79 133
80 // Get all roles (Should be the most limited dimension) 134 // Get all roles (Should be the most limited dimension)
81 $roles = $this->role->with('permissions')->get(); 135 $roles = $this->role->with('permissions')->get();
...@@ -97,7 +151,7 @@ class PermissionService ...@@ -97,7 +151,7 @@ class PermissionService
97 } 151 }
98 152
99 /** 153 /**
100 - * Create the entity jointPermissions for a particular entity. 154 + * Rebuild the entity jointPermissions for a particular entity.
101 * @param Entity $entity 155 * @param Entity $entity
102 */ 156 */
103 public function buildJointPermissionsForEntity(Entity $entity) 157 public function buildJointPermissionsForEntity(Entity $entity)
...@@ -117,6 +171,17 @@ class PermissionService ...@@ -117,6 +171,17 @@ class PermissionService
117 } 171 }
118 172
119 /** 173 /**
174 + * Rebuild the entity jointPermissions for a collection of entities.
175 + * @param Collection $entities
176 + */
177 + public function buildJointPermissionsForEntities(Collection $entities)
178 + {
179 + $roles = $this->role->with('jointPermissions')->get();
180 + $this->deleteManyJointPermissionsForEntities($entities);
181 + $this->createManyJointPermissions($entities, $roles);
182 + }
183 +
184 + /**
120 * Build the entity jointPermissions for a particular role. 185 * Build the entity jointPermissions for a particular role.
121 * @param Role $role 186 * @param Role $role
122 */ 187 */
...@@ -177,9 +242,14 @@ class PermissionService ...@@ -177,9 +242,14 @@ class PermissionService
177 */ 242 */
178 protected function deleteManyJointPermissionsForEntities($entities) 243 protected function deleteManyJointPermissionsForEntities($entities)
179 { 244 {
245 + $query = $this->jointPermission->newQuery();
180 foreach ($entities as $entity) { 246 foreach ($entities as $entity) {
181 - $entity->jointPermissions()->delete(); 247 + $query->orWhere(function($query) use ($entity) {
248 + $query->where('entity_id', '=', $entity->id)
249 + ->where('entity_type', '=', $entity->getMorphClass());
250 + });
182 } 251 }
252 + $query->delete();
183 } 253 }
184 254
185 /** 255 /**
...@@ -189,6 +259,7 @@ class PermissionService ...@@ -189,6 +259,7 @@ class PermissionService
189 */ 259 */
190 protected function createManyJointPermissions($entities, $roles) 260 protected function createManyJointPermissions($entities, $roles)
191 { 261 {
262 + $this->readyEntityCache();
192 $jointPermissions = []; 263 $jointPermissions = [];
193 foreach ($entities as $entity) { 264 foreach ($entities as $entity) {
194 foreach ($roles as $role) { 265 foreach ($roles as $role) {
...@@ -248,8 +319,9 @@ class PermissionService ...@@ -248,8 +319,9 @@ class PermissionService
248 } elseif ($entity->isA('chapter')) { 319 } elseif ($entity->isA('chapter')) {
249 320
250 if (!$entity->restricted) { 321 if (!$entity->restricted) {
251 - $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); 322 + $book = $this->getBook($entity->book_id);
252 - $hasPermissiveAccessToBook = !$entity->book->restricted; 323 + $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
324 + $hasPermissiveAccessToBook = !$book->restricted;
253 return $this->createJointPermissionDataArray($entity, $role, $action, 325 return $this->createJointPermissionDataArray($entity, $role, $action,
254 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)), 326 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
255 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook))); 327 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
...@@ -261,11 +333,14 @@ class PermissionService ...@@ -261,11 +333,14 @@ class PermissionService
261 } elseif ($entity->isA('page')) { 333 } elseif ($entity->isA('page')) {
262 334
263 if (!$entity->restricted) { 335 if (!$entity->restricted) {
264 - $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); 336 + $book = $this->getBook($entity->book_id);
265 - $hasPermissiveAccessToBook = !$entity->book->restricted; 337 + $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
266 - $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction); 338 + $hasPermissiveAccessToBook = !$book->restricted;
267 - $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted; 339 +
268 - $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted); 340 + $chapter = $this->getChapter($entity->chapter_id);
341 + $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
342 + $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
343 + $acknowledgeChapter = ($chapter && $chapter->restricted);
269 344
270 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook; 345 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
271 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook; 346 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
......
...@@ -158,7 +158,7 @@ class SocialAuthService ...@@ -158,7 +158,7 @@ class SocialAuthService
158 $driver = trim(strtolower($socialDriver)); 158 $driver = trim(strtolower($socialDriver));
159 159
160 if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found'); 160 if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
161 - if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured; 161 + if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
162 162
163 return $driver; 163 return $driver;
164 } 164 }
......
...@@ -2,33 +2,38 @@ ...@@ -2,33 +2,38 @@
2 2
3 use BookStack\Ownable; 3 use BookStack\Ownable;
4 4
5 -if (!function_exists('versioned_asset')) { 5 +/**
6 - /** 6 + * Get the path to a versioned file.
7 - * Get the path to a versioned file. 7 + *
8 - * 8 + * @param string $file
9 - * @param string $file 9 + * @return string
10 - * @return string 10 + * @throws Exception
11 - * 11 + */
12 - * @throws \InvalidArgumentException 12 +function versioned_asset($file = '')
13 - */ 13 +{
14 - function versioned_asset($file) 14 + // Don't require css and JS assets for testing
15 - { 15 + if (config('app.env') === 'testing') return '';
16 - static $manifest = null; 16 +
17 - 17 + static $manifest = null;
18 - if (is_null($manifest)) { 18 + $manifestPath = 'build/manifest.json';
19 - $manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true); 19 +
20 - } 20 + if (is_null($manifest) && file_exists($manifestPath)) {
21 - 21 + $manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
22 - if (isset($manifest[$file])) { 22 + } else if (!file_exists($manifestPath)) {
23 - return baseUrl($manifest[$file]); 23 + if (config('app.env') !== 'production') {
24 - } 24 + $path = public_path($manifestPath);
25 - 25 + $error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
26 - if (file_exists(public_path($file))) { 26 + } else {
27 - return baseUrl($file); 27 + $error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
28 } 28 }
29 + throw new \Exception($error);
30 + }
29 31
30 - throw new InvalidArgumentException("File {$file} not defined in asset manifest."); 32 + if (isset($manifest[$file])) {
33 + return baseUrl($manifest[$file]);
31 } 34 }
35 +
36 + throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
32 } 37 }
33 38
34 /** 39 /**
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddSummaryToPageRevisions extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('page_revisions', function ($table) {
16 + $table->string('summary')->nullable();
17 + });
18 + }
19 +
20 + /**
21 + * Reverse the migrations.
22 + *
23 + * @return void
24 + */
25 + public function down()
26 + {
27 + Schema::table('page_revisions', function ($table) {
28 + $table->dropColumn('summary');
29 + });
30 + }
31 +}
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
28 <env name="DB_CONNECTION" value="mysql_testing"/> 28 <env name="DB_CONNECTION" value="mysql_testing"/>
29 <env name="MAIL_DRIVER" value="log"/> 29 <env name="MAIL_DRIVER" value="log"/>
30 <env name="AUTH_METHOD" value="standard"/> 30 <env name="AUTH_METHOD" value="standard"/>
31 - <env name="DISABLE_EXTERNAL_SERVICES" value="false"/> 31 + <env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
32 <env name="LDAP_VERSION" value="3"/> 32 <env name="LDAP_VERSION" value="3"/>
33 <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/> 33 <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
34 <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/> 34 <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
......
...@@ -69,7 +69,7 @@ module.exports = function (ngApp, events) { ...@@ -69,7 +69,7 @@ module.exports = function (ngApp, events) {
69 */ 69 */
70 function callbackAndHide(returnData) { 70 function callbackAndHide(returnData) {
71 if (callback) callback(returnData); 71 if (callback) callback(returnData);
72 - $scope.showing = false; 72 + $scope.hide();
73 } 73 }
74 74
75 /** 75 /**
...@@ -109,6 +109,7 @@ module.exports = function (ngApp, events) { ...@@ -109,6 +109,7 @@ module.exports = function (ngApp, events) {
109 function show(doneCallback) { 109 function show(doneCallback) {
110 callback = doneCallback; 110 callback = doneCallback;
111 $scope.showing = true; 111 $scope.showing = true;
112 + $('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
112 // Get initial images if they have not yet been loaded in. 113 // Get initial images if they have not yet been loaded in.
113 if (!dataLoaded) { 114 if (!dataLoaded) {
114 fetchData(); 115 fetchData();
...@@ -131,6 +132,7 @@ module.exports = function (ngApp, events) { ...@@ -131,6 +132,7 @@ module.exports = function (ngApp, events) {
131 */ 132 */
132 $scope.hide = function () { 133 $scope.hide = function () {
133 $scope.showing = false; 134 $scope.showing = false;
135 + $('#image-manager').find('.overlay').fadeOut(240);
134 }; 136 };
135 137
136 var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/'); 138 var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
...@@ -357,8 +359,6 @@ module.exports = function (ngApp, events) { ...@@ -357,8 +359,6 @@ module.exports = function (ngApp, events) {
357 359
358 /** 360 /**
359 * Save a draft update into the system via an AJAX request. 361 * Save a draft update into the system via an AJAX request.
360 - * @param title
361 - * @param html
362 */ 362 */
363 function saveDraft() { 363 function saveDraft() {
364 var data = { 364 var data = {
...@@ -373,9 +373,17 @@ module.exports = function (ngApp, events) { ...@@ -373,9 +373,17 @@ module.exports = function (ngApp, events) {
373 var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate(); 373 var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
374 $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm'); 374 $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
375 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; 375 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
376 + showDraftSaveNotification();
376 }); 377 });
377 } 378 }
378 379
380 + function showDraftSaveNotification() {
381 + $scope.draftUpdated = true;
382 + $timeout(() => {
383 + $scope.draftUpdated = false;
384 + }, 2000)
385 + }
386 +
379 $scope.forceDraftSave = function() { 387 $scope.forceDraftSave = function() {
380 saveDraft(); 388 saveDraft();
381 }; 389 };
......
...@@ -18,9 +18,12 @@ window.baseUrl = function(path) { ...@@ -18,9 +18,12 @@ window.baseUrl = function(path) {
18 var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); 18 var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
19 19
20 // Global Event System 20 // Global Event System
21 -var Events = { 21 +class EventManager {
22 - listeners: {}, 22 + constructor() {
23 - emit: function (eventName, eventData) { 23 + this.listeners = {};
24 + }
25 +
26 + emit(eventName, eventData) {
24 if (typeof this.listeners[eventName] === 'undefined') return this; 27 if (typeof this.listeners[eventName] === 'undefined') return this;
25 var eventsToStart = this.listeners[eventName]; 28 var eventsToStart = this.listeners[eventName];
26 for (let i = 0; i < eventsToStart.length; i++) { 29 for (let i = 0; i < eventsToStart.length; i++) {
...@@ -28,33 +31,35 @@ var Events = { ...@@ -28,33 +31,35 @@ var Events = {
28 event(eventData); 31 event(eventData);
29 } 32 }
30 return this; 33 return this;
31 - }, 34 + }
32 - listen: function (eventName, callback) { 35 +
36 + listen(eventName, callback) {
33 if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; 37 if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
34 this.listeners[eventName].push(callback); 38 this.listeners[eventName].push(callback);
35 return this; 39 return this;
36 } 40 }
37 }; 41 };
38 -window.Events = Events; 42 +window.Events = new EventManager();
39 43
40 44
41 -var services = require('./services')(ngApp, Events); 45 +var services = require('./services')(ngApp, window.Events);
42 -var directives = require('./directives')(ngApp, Events); 46 +var directives = require('./directives')(ngApp, window.Events);
43 -var controllers = require('./controllers')(ngApp, Events); 47 +var controllers = require('./controllers')(ngApp, window.Events);
44 48
45 //Global jQuery Config & Extensions 49 //Global jQuery Config & Extensions
46 50
47 // Smooth scrolling 51 // Smooth scrolling
48 jQuery.fn.smoothScrollTo = function () { 52 jQuery.fn.smoothScrollTo = function () {
49 if (this.length === 0) return; 53 if (this.length === 0) return;
50 - $('body').animate({ 54 + let scrollElem = document.documentElement.scrollTop === 0 ? document.body : document.documentElement;
55 + $(scrollElem).animate({
51 scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin 56 scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
52 }, 800); // Adjust to change animations speed (ms) 57 }, 800); // Adjust to change animations speed (ms)
53 return this; 58 return this;
54 }; 59 };
55 60
56 // Making contains text expression not worry about casing 61 // Making contains text expression not worry about casing
57 -$.expr[":"].contains = $.expr.createPseudo(function (arg) { 62 +jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
58 return function (elem) { 63 return function (elem) {
59 return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; 64 return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
60 }; 65 };
...@@ -104,13 +109,14 @@ $(function () { ...@@ -104,13 +109,14 @@ $(function () {
104 var scrollTop = document.getElementById('back-to-top'); 109 var scrollTop = document.getElementById('back-to-top');
105 var scrollTopBreakpoint = 1200; 110 var scrollTopBreakpoint = 1200;
106 window.addEventListener('scroll', function() { 111 window.addEventListener('scroll', function() {
107 - if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) { 112 + let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
113 + if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
108 scrollTop.style.display = 'block'; 114 scrollTop.style.display = 'block';
109 scrollTopShowing = true; 115 scrollTopShowing = true;
110 setTimeout(() => { 116 setTimeout(() => {
111 scrollTop.style.opacity = 0.4; 117 scrollTop.style.opacity = 0.4;
112 }, 1); 118 }, 1);
113 - } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) { 119 + } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
114 scrollTop.style.opacity = 0; 120 scrollTop.style.opacity = 0;
115 scrollTopShowing = false; 121 scrollTopShowing = false;
116 setTimeout(() => { 122 setTimeout(() => {
...@@ -124,6 +130,27 @@ $(function () { ...@@ -124,6 +130,27 @@ $(function () {
124 $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240); 130 $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
125 }); 131 });
126 132
133 + // Popup close
134 + $('.popup-close').click(function() {
135 + $(this).closest('.overlay').fadeOut(240);
136 + });
137 + $('.overlay').click(function(event) {
138 + if (!$(event.target).hasClass('overlay')) return;
139 + $(this).fadeOut(240);
140 + });
141 +
142 + // Prevent markdown display link click redirect
143 + $('.markdown-display').on('click', 'a', function(event) {
144 + event.preventDefault();
145 + window.open($(this).attr('href'));
146 + });
147 +
148 + // Detect IE for css
149 + if(navigator.userAgent.indexOf('MSIE')!==-1
150 + || navigator.appVersion.indexOf('Trident/') > 0
151 + || navigator.userAgent.indexOf('Safari') !== -1){
152 + $('body').addClass('flexbox-support');
153 + }
127 154
128 }); 155 });
129 156
......
1 +"use strict";
2 +
3 +/**
4 + * Handle pasting images from clipboard.
5 + * @param e - event
6 + * @param editor - editor instance
7 + */
8 +function editorPaste(e, editor) {
9 + if (!e.clipboardData) return
10 + let items = e.clipboardData.items;
11 + if (!items) return;
12 + for (let i = 0; i < items.length; i++) {
13 + if (items[i].type.indexOf("image") === -1) return
14 +
15 + let file = items[i].getAsFile();
16 + let formData = new FormData();
17 + let ext = 'png';
18 + let xhr = new XMLHttpRequest();
19 +
20 + if (file.name) {
21 + let fileNameMatches = file.name.match(/\.(.+)$/);
22 + if (fileNameMatches) {
23 + ext = fileNameMatches[1];
24 + }
25 + }
26 +
27 + let id = "image-" + Math.random().toString(16).slice(2);
28 + let loadingImage = window.baseUrl('/loading.gif');
29 + editor.execCommand('mceInsertContent', false, `<img src="${loadingImage}" id="${id}">`);
30 +
31 + let remoteFilename = "image-" + Date.now() + "." + ext;
32 + formData.append('file', file, remoteFilename);
33 + formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
34 +
35 + xhr.open('POST', window.baseUrl('/images/gallery/upload'));
36 + xhr.onload = function () {
37 + if (xhr.status === 200 || xhr.status === 201) {
38 + let result = JSON.parse(xhr.responseText);
39 + editor.dom.setAttrib(id, 'src', result.thumbs.display);
40 + } else {
41 + console.log('An error occurred uploading the image', xhr.responseText);
42 + editor.dom.remove(id);
43 + }
44 + };
45 + xhr.send(formData);
46 +
47 + }
48 +}
49 +
50 +function registerEditorShortcuts(editor) {
51 + // Headers
52 + for (let i = 1; i < 5; i++) {
53 + editor.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
54 + }
55 +
56 + // Other block shortcuts
57 + editor.addShortcut('ctrl+q', '', ['FormatBlock', false, 'blockquote']);
58 + editor.addShortcut('ctrl+d', '', ['FormatBlock', false, 'p']);
59 + editor.addShortcut('ctrl+e', '', ['FormatBlock', false, 'pre']);
60 + editor.addShortcut('ctrl+s', '', ['FormatBlock', false, 'code']);
61 +}
62 +
1 var mceOptions = module.exports = { 63 var mceOptions = module.exports = {
2 selector: '#html-editor', 64 selector: '#html-editor',
3 content_css: [ 65 content_css: [
...@@ -6,6 +68,8 @@ var mceOptions = module.exports = { ...@@ -6,6 +68,8 @@ var mceOptions = module.exports = {
6 ], 68 ],
7 body_class: 'page-content', 69 body_class: 'page-content',
8 relative_urls: false, 70 relative_urls: false,
71 + remove_script_host: false,
72 + document_base_url: window.baseUrl('/'),
9 statusbar: false, 73 statusbar: false,
10 menubar: false, 74 menubar: false,
11 paste_data_images: false, 75 paste_data_images: false,
...@@ -38,23 +102,41 @@ var mceOptions = module.exports = { ...@@ -38,23 +102,41 @@ var mceOptions = module.exports = {
38 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, 102 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
39 }, 103 },
40 file_browser_callback: function (field_name, url, type, win) { 104 file_browser_callback: function (field_name, url, type, win) {
41 - window.ImageManager.showExternal(function (image) { 105 +
42 - win.document.getElementById(field_name).value = image.url; 106 + if (type === 'file') {
43 - if ("createEvent" in document) { 107 + window.showEntityLinkSelector(function(entity) {
44 - var evt = document.createEvent("HTMLEvents"); 108 + let originalField = win.document.getElementById(field_name);
45 - evt.initEvent("change", false, true); 109 + originalField.value = entity.link;
46 - win.document.getElementById(field_name).dispatchEvent(evt); 110 + $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
47 - } else { 111 + });
48 - win.document.getElementById(field_name).fireEvent("onchange"); 112 + }
49 - } 113 +
50 - var html = '<a href="' + image.url + '" target="_blank">'; 114 + if (type === 'image') {
51 - html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">'; 115 + // Show image manager
52 - html += '</a>'; 116 + window.ImageManager.showExternal(function (image) {
53 - win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); 117 +
54 - }); 118 + // Set popover link input to image url then fire change event
119 + // to ensure the new value sticks
120 + win.document.getElementById(field_name).value = image.url;
121 + if ("createEvent" in document) {
122 + let evt = document.createEvent("HTMLEvents");
123 + evt.initEvent("change", false, true);
124 + win.document.getElementById(field_name).dispatchEvent(evt);
125 + } else {
126 + win.document.getElementById(field_name).fireEvent("onchange");
127 + }
128 +
129 + // Replace the actively selected content with the linked image
130 + let html = `<a href="${image.url}" target="_blank">`;
131 + html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
132 + html += '</a>';
133 + win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
134 + });
135 + }
136 +
55 }, 137 },
56 paste_preprocess: function (plugin, args) { 138 paste_preprocess: function (plugin, args) {
57 - var content = args.content; 139 + let content = args.content;
58 if (content.indexOf('<img src="file://') !== -1) { 140 if (content.indexOf('<img src="file://') !== -1) {
59 args.content = ''; 141 args.content = '';
60 } 142 }
...@@ -62,10 +144,14 @@ var mceOptions = module.exports = { ...@@ -62,10 +144,14 @@ var mceOptions = module.exports = {
62 extraSetups: [], 144 extraSetups: [],
63 setup: function (editor) { 145 setup: function (editor) {
64 146
65 - for (var i = 0; i < mceOptions.extraSetups.length; i++) { 147 + // Run additional setup actions
148 + // Used by the angular side of things
149 + for (let i = 0; i < mceOptions.extraSetups.length; i++) {
66 mceOptions.extraSetups[i](editor); 150 mceOptions.extraSetups[i](editor);
67 } 151 }
68 152
153 + registerEditorShortcuts(editor);
154 +
69 (function () { 155 (function () {
70 var wrap; 156 var wrap;
71 157
...@@ -76,12 +162,11 @@ var mceOptions = module.exports = { ...@@ -76,12 +162,11 @@ var mceOptions = module.exports = {
76 editor.on('dragstart', function () { 162 editor.on('dragstart', function () {
77 var node = editor.selection.getNode(); 163 var node = editor.selection.getNode();
78 164
79 - if (node.nodeName === 'IMG') { 165 + if (node.nodeName !== 'IMG') return;
80 - wrap = editor.dom.getParent(node, '.mceTemp'); 166 + wrap = editor.dom.getParent(node, '.mceTemp');
81 167
82 - if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) { 168 + if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
83 - wrap = node.parentNode; 169 + wrap = node.parentNode;
84 - }
85 } 170 }
86 }); 171 });
87 172
...@@ -106,15 +191,15 @@ var mceOptions = module.exports = { ...@@ -106,15 +191,15 @@ var mceOptions = module.exports = {
106 }); 191 });
107 })(); 192 })();
108 193
109 - // Image picker button 194 + // Custom Image picker button
110 editor.addButton('image-insert', { 195 editor.addButton('image-insert', {
111 title: 'My title', 196 title: 'My title',
112 icon: 'image', 197 icon: 'image',
113 tooltip: 'Insert an image', 198 tooltip: 'Insert an image',
114 onclick: function () { 199 onclick: function () {
115 window.ImageManager.showExternal(function (image) { 200 window.ImageManager.showExternal(function (image) {
116 - var html = '<a href="' + image.url + '" target="_blank">'; 201 + let html = `<a href="${image.url}" target="_blank">`;
117 - html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">'; 202 + html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
118 html += '</a>'; 203 html += '</a>';
119 editor.execCommand('mceInsertContent', false, html); 204 editor.execCommand('mceInsertContent', false, html);
120 }); 205 });
...@@ -122,49 +207,8 @@ var mceOptions = module.exports = { ...@@ -122,49 +207,8 @@ var mceOptions = module.exports = {
122 }); 207 });
123 208
124 // Paste image-uploads 209 // Paste image-uploads
125 - editor.on('paste', function (e) { 210 + editor.on('paste', function(event) {
126 - if (e.clipboardData) { 211 + editorPaste(event, editor);
127 - var items = e.clipboardData.items;
128 - if (items) {
129 - for (var i = 0; i < items.length; i++) {
130 - if (items[i].type.indexOf("image") !== -1) {
131 -
132 - var file = items[i].getAsFile();
133 - var formData = new FormData();
134 - var ext = 'png';
135 - var xhr = new XMLHttpRequest();
136 -
137 - if (file.name) {
138 - var fileNameMatches = file.name.match(/\.(.+)$/);
139 - if (fileNameMatches) {
140 - ext = fileNameMatches[1];
141 - }
142 - }
143 -
144 - var id = "image-" + Math.random().toString(16).slice(2);
145 - editor.execCommand('mceInsertContent', false, '<img src="/loading.gif" id="' + id + '">');
146 -
147 - var remoteFilename = "image-" + Date.now() + "." + ext;
148 - formData.append('file', file, remoteFilename);
149 - formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
150 -
151 - xhr.open('POST', window.baseUrl('/images/gallery/upload'));
152 - xhr.onload = function () {
153 - if (xhr.status === 200 || xhr.status === 201) {
154 - var result = JSON.parse(xhr.responseText);
155 - editor.dom.setAttrib(id, 'src', result.url);
156 - } else {
157 - console.log('An error occured uploading the image');
158 - console.log(xhr.responseText);
159 - editor.dom.remove(id);
160 - }
161 - };
162 - xhr.send(formData);
163 - }
164 - }
165 - }
166 -
167 - }
168 }); 212 });
169 } 213 }
170 }; 214 };
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -100,3 +100,13 @@ $button-border-radius: 2px; ...@@ -100,3 +100,13 @@ $button-border-radius: 2px;
100 } 100 }
101 } 101 }
102 102
103 +.button[disabled] {
104 + background-color: #BBB;
105 + cursor: default;
106 + &:hover {
107 + background-color: #BBB;
108 + cursor: default;
109 + box-shadow: none;
110 + }
111 +}
112 +
......
1 .overlay { 1 .overlay {
2 - background-color: rgba(0, 0, 0, 0.2); 2 + background-color: rgba(0, 0, 0, 0.333);
3 position: fixed; 3 position: fixed;
4 z-index: 95536; 4 z-index: 95536;
5 width: 100%; 5 width: 100%;
...@@ -10,37 +10,81 @@ ...@@ -10,37 +10,81 @@
10 left: 0; 10 left: 0;
11 right: 0; 11 right: 0;
12 bottom: 0; 12 bottom: 0;
13 + display: flex;
14 + align-items: center;
15 + justify-content: center;
16 + display: none;
13 } 17 }
14 18
15 -.image-manager-body { 19 +.popup-body-wrap {
20 + display: flex;
21 +}
22 +
23 +.popup-body {
16 background-color: #FFF; 24 background-color: #FFF;
17 max-height: 90%; 25 max-height: 90%;
18 - width: 90%; 26 + width: 1200px;
19 - height: 90%; 27 + height: auto;
20 margin: 2% 5%; 28 margin: 2% 5%;
21 border-radius: 4px; 29 border-radius: 4px;
22 box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); 30 box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
23 overflow: hidden; 31 overflow: hidden;
24 - position: fixed;
25 - top: 0;
26 - bottom: 0;
27 - left: 0;
28 z-index: 999; 32 z-index: 999;
29 display: flex; 33 display: flex;
30 - h1, h2, h3 { 34 + flex-direction: column;
31 - font-weight: 300; 35 + &.small {
36 + margin: 2% auto;
37 + width: 800px;
38 + max-width: 90%;
39 + }
40 + &:before {
41 + display: flex;
42 + align-self: flex-start;
32 } 43 }
33 } 44 }
34 45
35 -#image-manager .dropzone-container { 46 +//body.ie .popup-body {
36 - position: relative; 47 +// min-height: 100%;
37 - border: 3px dashed #DDD; 48 +//}
38 -}
39 49
40 -.image-manager-bottom { 50 +.corner-button {
41 position: absolute; 51 position: absolute;
42 - bottom: 0; 52 + top: 0;
43 right: 0; 53 right: 0;
54 + margin: 0;
55 + height: 40px;
56 + border-radius: 0;
57 + box-shadow: none;
58 +}
59 +
60 +.popup-header, .popup-footer {
61 + display: block !important;
62 + position: relative;
63 + height: 40px;
64 + flex: none !important;
65 + .popup-title {
66 + color: #FFF;
67 + padding: 8px $-m;
68 + }
69 +}
70 +body.flexbox-support #entity-selector-wrap .popup-body .form-group {
71 + height: 444px;
72 + min-height: 444px;
73 +}
74 +#entity-selector-wrap .popup-body .form-group {
75 + margin: 0;
76 +}
77 +//body.ie #entity-selector-wrap .popup-body .form-group {
78 +// min-height: 60vh;
79 +//}
80 +
81 +.image-manager-body {
82 + min-height: 70vh;
83 +}
84 +
85 +#image-manager .dropzone-container {
86 + position: relative;
87 + border: 3px dashed #DDD;
44 } 88 }
45 89
46 .image-manager-list .image { 90 .image-manager-list .image {
...@@ -103,18 +147,13 @@ ...@@ -103,18 +147,13 @@
103 147
104 .image-manager-sidebar { 148 .image-manager-sidebar {
105 width: 300px; 149 width: 300px;
106 - height: 100%;
107 margin-left: 1px; 150 margin-left: 1px;
108 - padding: 0 $-l; 151 + padding: $-m $-l;
152 + overflow-y: auto;
109 border-left: 1px solid #DDD; 153 border-left: 1px solid #DDD;
110 -} 154 + .dropzone-container {
111 - 155 + margin-top: $-m;
112 -.image-manager-close { 156 + }
113 - position: absolute;
114 - top: 0;
115 - right: 0;
116 - margin: 0;
117 - border-radius: 0;
118 } 157 }
119 158
120 .image-manager-list { 159 .image-manager-list {
...@@ -125,7 +164,6 @@ ...@@ -125,7 +164,6 @@
125 .image-manager-content { 164 .image-manager-content {
126 display: flex; 165 display: flex;
127 flex-direction: column; 166 flex-direction: column;
128 - height: 100%;
129 flex: 1; 167 flex: 1;
130 .container { 168 .container {
131 width: 100%; 169 width: 100%;
...@@ -141,12 +179,13 @@ ...@@ -141,12 +179,13 @@
141 * Copyright (c) 2012 Matias Meno <m@tias.me> 179 * Copyright (c) 2012 Matias Meno <m@tias.me>
142 */ 180 */
143 .dz-message { 181 .dz-message {
144 - font-size: 1.4em; 182 + font-size: 1.2em;
183 + line-height: 1.1;
145 font-style: italic; 184 font-style: italic;
146 color: #aaa; 185 color: #aaa;
147 text-align: center; 186 text-align: center;
148 cursor: pointer; 187 cursor: pointer;
149 - padding: $-xl $-m; 188 + padding: $-l $-m;
150 transition: all ease-in-out 120ms; 189 transition: all ease-in-out 120ms;
151 } 190 }
152 191
......
...@@ -25,6 +25,14 @@ body.flexbox { ...@@ -25,6 +25,14 @@ body.flexbox {
25 } 25 }
26 } 26 }
27 27
28 +.flex-child > div {
29 + flex: 1;
30 +}
31 +
32 +//body.ie .flex-child > div {
33 +// flex: 1 0 0px;
34 +//}
35 +
28 /** Rules for all columns */ 36 /** Rules for all columns */
29 div[class^="col-"] img { 37 div[class^="col-"] img {
30 max-width: 100%; 38 max-width: 100%;
...@@ -39,6 +47,9 @@ div[class^="col-"] img { ...@@ -39,6 +47,9 @@ div[class^="col-"] img {
39 &.fluid { 47 &.fluid {
40 max-width: 100%; 48 max-width: 100%;
41 } 49 }
50 + &.medium {
51 + max-width: 992px;
52 + }
42 &.small { 53 &.small {
43 max-width: 840px; 54 max-width: 840px;
44 } 55 }
......
...@@ -155,6 +155,7 @@ form.search-box { ...@@ -155,6 +155,7 @@ form.search-box {
155 text-decoration: none; 155 text-decoration: none;
156 } 156 }
157 } 157 }
158 +
158 } 159 }
159 160
160 .faded span.faded-text { 161 .faded span.faded-text {
......
...@@ -375,6 +375,9 @@ ul.pagination { ...@@ -375,6 +375,9 @@ ul.pagination {
375 .text-muted { 375 .text-muted {
376 color: #999; 376 color: #999;
377 } 377 }
378 + li.padded {
379 + padding: $-xs $-m;
380 + }
378 a { 381 a {
379 display: block; 382 display: block;
380 padding: $-xs $-m; 383 padding: $-xs $-m;
...@@ -384,10 +387,10 @@ ul.pagination { ...@@ -384,10 +387,10 @@ ul.pagination {
384 background-color: #EEE; 387 background-color: #EEE;
385 } 388 }
386 i { 389 i {
387 - margin-right: $-m; 390 + margin-right: $-s;
388 padding-right: 0; 391 padding-right: 0;
389 - display: inline; 392 + display: inline-block;
390 - width: 22px; 393 + width: 16px;
391 } 394 }
392 } 395 }
393 li.border-bottom { 396 li.border-bottom {
......
...@@ -20,6 +20,16 @@ ...@@ -20,6 +20,16 @@
20 } 20 }
21 } 21 }
22 22
23 +.draft-notification {
24 + pointer-events: none;
25 + transform: scale(0);
26 + transition: transform ease-in-out 120ms;
27 + transform-origin: 50% 50%;
28 + &.visible {
29 + transform: scale(1);
30 + }
31 +}
32 +
23 .page-style.editor { 33 .page-style.editor {
24 padding: 0 !important; 34 padding: 0 !important;
25 } 35 }
...@@ -238,7 +248,7 @@ ...@@ -238,7 +248,7 @@
238 } 248 }
239 249
240 .tag-display { 250 .tag-display {
241 - margin: $-xl $-xs; 251 + margin: $-xl $-m;
242 border: 1px solid #DDD; 252 border: 1px solid #DDD;
243 min-width: 180px; 253 min-width: 180px;
244 max-width: 320px; 254 max-width: 320px;
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
12 @import "animations"; 12 @import "animations";
13 @import "tinymce"; 13 @import "tinymce";
14 @import "highlightjs"; 14 @import "highlightjs";
15 -@import "image-manager"; 15 +@import "components";
16 @import "header"; 16 @import "header";
17 @import "lists"; 17 @import "lists";
18 @import "pages"; 18 @import "pages";
...@@ -72,7 +72,7 @@ body.dragging, body.dragging * { ...@@ -72,7 +72,7 @@ body.dragging, body.dragging * {
72 border-radius: 3px; 72 border-radius: 3px;
73 box-shadow: $bs-med; 73 box-shadow: $bs-med;
74 z-index: 999999; 74 z-index: 999999;
75 - display: table; 75 + display: block;
76 cursor: pointer; 76 cursor: pointer;
77 max-width: 480px; 77 max-width: 480px;
78 i, span { 78 i, span {
......
1 <div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}"> 1 <div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
2 - <h3 class="text-book"><a class="text-book" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></h3> 2 + <h3 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h3>
3 @if(isset($book->searchSnippet)) 3 @if(isset($book->searchSnippet))
4 <p class="text-muted">{!! $book->searchSnippet !!}</p> 4 <p class="text-muted">{!! $book->searchSnippet !!}</p>
5 @else 5 @else
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
50 var sortableOptions = { 50 var sortableOptions = {
51 group: 'serialization', 51 group: 'serialization',
52 onDrop: function($item, container, _super) { 52 onDrop: function($item, container, _super) {
53 - var pageMap = buildPageMap(); 53 + var pageMap = buildEntityMap();
54 $('#sort-tree-input').val(JSON.stringify(pageMap)); 54 $('#sort-tree-input').val(JSON.stringify(pageMap));
55 _super($item, container); 55 _super($item, container);
56 }, 56 },
...@@ -74,29 +74,42 @@ ...@@ -74,29 +74,42 @@
74 $link.remove(); 74 $link.remove();
75 }); 75 });
76 76
77 - function buildPageMap() { 77 + /**
78 - var pageMap = []; 78 + * Build up a mapping of entities with their ordering and nesting.
79 + * @returns {Array}
80 + */
81 + function buildEntityMap() {
82 + var entityMap = [];
79 var $lists = $('.sort-list'); 83 var $lists = $('.sort-list');
80 $lists.each(function(listIndex) { 84 $lists.each(function(listIndex) {
81 var list = $(this); 85 var list = $(this);
82 var bookId = list.closest('[data-type="book"]').attr('data-id'); 86 var bookId = list.closest('[data-type="book"]').attr('data-id');
83 - var $childElements = list.find('[data-type="page"], [data-type="chapter"]'); 87 + var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
84 - $childElements.each(function(childIndex) { 88 + $directChildren.each(function(directChildIndex) {
85 var $childElem = $(this); 89 var $childElem = $(this);
86 var type = $childElem.attr('data-type'); 90 var type = $childElem.attr('data-type');
87 var parentChapter = false; 91 var parentChapter = false;
88 - if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) { 92 + var childId = $childElem.attr('data-id');
89 - parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id'); 93 + entityMap.push({
90 - } 94 + id: childId,
91 - pageMap.push({ 95 + sort: directChildIndex,
92 - id: $childElem.attr('data-id'),
93 parentChapter: parentChapter, 96 parentChapter: parentChapter,
94 type: type, 97 type: type,
95 book: bookId 98 book: bookId
96 }); 99 });
100 + $chapterChildren = $childElem.find('[data-type="page"]').each(function(pageIndex) {
101 + var $chapterChild = $(this);
102 + entityMap.push({
103 + id: $chapterChild.attr('data-id'),
104 + sort: pageIndex,
105 + parentChapter: childId,
106 + type: 'page',
107 + book: bookId
108 + });
109 + });
97 }); 110 });
98 }); 111 });
99 - return pageMap; 112 + return entityMap;
100 } 113 }
101 114
102 }); 115 });
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
6 </a> 6 </a>
7 <span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span> 7 <span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span>
8 @endif 8 @endif
9 - <a href="{{ $chapter->getUrl() }}" class="text-chapter"> 9 + <a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
10 - <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }} 10 + <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
11 </a> 11 </a>
12 </h3> 12 </h3>
13 @if(isset($chapter->searchSnippet)) 13 @if(isset($chapter->searchSnippet))
......
...@@ -19,6 +19,14 @@ ...@@ -19,6 +19,14 @@
19 19
20 20
21 </div> 21 </div>
22 +
22 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) 23 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
24 + @include('partials/entity-selector-popup')
25 +
26 + <script>
27 + (function() {
28 +
29 + })();
30 + </script>
23 31
24 @stop 32 @stop
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -13,8 +13,9 @@ ...@@ -13,8 +13,9 @@
13 </div> 13 </div>
14 <div class="col-sm-4 faded text-center"> 14 <div class="col-sm-4 faded text-center">
15 15
16 - <div dropdown class="dropdown-container"> 16 + <div dropdown class="dropdown-container draft-display">
17 <a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a> 17 <a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a>
18 + <i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
18 <ul> 19 <ul>
19 <li> 20 <li>
20 <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a> 21 <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
...@@ -22,14 +23,25 @@ ...@@ -22,14 +23,25 @@
22 <li ng-if="isNewPageDraft"> 23 <li ng-if="isNewPageDraft">
23 <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a> 24 <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
24 </li> 25 </li>
26 + <li>
27 + <a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</a>
28 + </li>
25 </ul> 29 </ul>
26 </div> 30 </div>
27 </div> 31 </div>
28 <div class="col-sm-4 faded"> 32 <div class="col-sm-4 faded">
29 <div class="action-buttons" ng-cloak> 33 <div class="action-buttons" ng-cloak>
34 + <div dropdown class="dropdown-container">
35 + <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> @{{(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || 'Set Changelog'}}</a>
36 + <ul class="wide">
37 + <li class="padded">
38 + <p class="text-muted">Enter a brief description of the changes you've made</p>
39 + <input name="summary" id="summary-input" type="text" placeholder="Enter Changelog" ng-model="changeSummary" />
40 + </li>
41 + </ul>
42 + </div>
30 43
31 - <button type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button> 44 + <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
32 - <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
33 </div> 45 </div>
34 </div> 46 </div>
35 </div> 47 </div>
...@@ -62,6 +74,8 @@ ...@@ -62,6 +74,8 @@
62 <span class="float left">Editor</span> 74 <span class="float left">Editor</span>
63 <div class="float right buttons"> 75 <div class="float right buttons">
64 <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button> 76 <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
77 + &nbsp;|&nbsp;
78 + <button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
65 </div> 79 </div>
66 </div> 80 </div>
67 81
......
1 <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}"> 1 <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
2 <h3> 2 <h3>
3 - <a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a> 3 + <a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
4 </h3> 4 </h3>
5 5
6 @if(isset($page->searchSnippet)) 6 @if(isset($page->searchSnippet))
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
16 </a> 16 </a>
17 @endif 17 @endif
18 <span class="sep">&raquo;</span> 18 <span class="sep">&raquo;</span>
19 - <a href="{{ $page->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a> 19 + <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
20 </div> 20 </div>
21 </div> 21 </div>
22 </div> 22 </div>
......
...@@ -5,45 +5,59 @@ ...@@ -5,45 +5,59 @@
5 <div class="faded-small toolbar"> 5 <div class="faded-small toolbar">
6 <div class="container"> 6 <div class="container">
7 <div class="row"> 7 <div class="row">
8 - <div class="col-md-6 faded"> 8 + <div class="col-sm-12 faded">
9 <div class="breadcrumbs"> 9 <div class="breadcrumbs">
10 - <a href="{{ $page->getUrl() }}" class="text-primary text-button"><i class="zmdi zmdi-arrow-left"></i>Back to page</a> 10 + <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
11 + @if($page->hasChapter())
12 + <span class="sep">&raquo;</span>
13 + <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
14 + <i class="zmdi zmdi-collection-bookmark"></i>
15 + {{ $page->chapter->getShortName() }}
16 + </a>
17 + @endif
18 + <span class="sep">&raquo;</span>
19 + <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
11 </div> 20 </div>
12 </div> 21 </div>
13 - <div class="col-md-6 faded">
14 - </div>
15 </div> 22 </div>
16 </div> 23 </div>
17 </div> 24 </div>
18 25
19 26
20 - <div class="container small" ng-non-bindable> 27 +
28 + <div class="container" ng-non-bindable>
21 <h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1> 29 <h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1>
22 30
23 @if(count($page->revisions) > 0) 31 @if(count($page->revisions) > 0)
24 32
25 <table class="table"> 33 <table class="table">
26 <tr> 34 <tr>
27 - <th width="40%">Name</th> 35 + <th width="25%">Name</th>
28 - <th colspan="2" width="20%">Created By</th> 36 + <th colspan="2" width="10%">Created By</th>
29 - <th width="20%">Revision Date</th> 37 + <th width="15%">Revision Date</th>
30 - <th width="20%">Actions</th> 38 + <th width="25%">Changelog</th>
39 + <th width="15%">Actions</th>
31 </tr> 40 </tr>
32 - @foreach($page->revisions as $revision) 41 + @foreach($page->revisions as $index => $revision)
33 <tr> 42 <tr>
34 - <td>{{$revision->name}}</td> 43 + <td>{{ $revision->name }}</td>
35 <td style="line-height: 0;"> 44 <td style="line-height: 0;">
36 @if($revision->createdBy) 45 @if($revision->createdBy)
37 - <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{$revision->createdBy->name}}"> 46 + <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
38 @endif 47 @endif
39 </td> 48 </td>
40 - <td> @if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif</td> 49 + <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
41 - <td><small>{{$revision->created_at->format('jS F, Y H:i:s')}} <br> ({{$revision->created_at->diffForHumans()}})</small></td> 50 + <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
42 - <td> 51 + <td>{{ $revision->summary }}</td>
43 - <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a> 52 + @if ($index !== 0)
44 - <span class="text-muted">&nbsp;|&nbsp;</span> 53 + <td>
45 - <a href="{{ $revision->getUrl('/restore') }}">Restore</a> 54 + <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
46 - </td> 55 + <span class="text-muted">&nbsp;|&nbsp;</span>
56 + <a href="{{ $revision->getUrl() }}/restore">Restore</a>
57 + </td>
58 + @else
59 + <td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
60 + @endif
47 </tr> 61 </tr>
48 @endforeach 62 @endforeach
49 </table> 63 </table>
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
58 <div class="container" id="page-show" ng-non-bindable> 58 <div class="container" id="page-show" ng-non-bindable>
59 <div class="row"> 59 <div class="row">
60 <div class="col-md-9 print-full-width"> 60 <div class="col-md-9 print-full-width">
61 - <div class="page-content anim fadeIn"> 61 + <div class="page-content">
62 62
63 <div class="pointer-container" id="pointer"> 63 <div class="pointer-container" id="pointer">
64 <div class="pointer anim"> 64 <div class="pointer anim">
......
1 +<div id="entity-selector-wrap">
2 + <div class="overlay" entity-link-selector>
3 + <div class="popup-body small flex-child">
4 + <div class="popup-header primary-background">
5 + <div class="popup-title">Entity Select</div>
6 + <button type="button" class="corner-button neg button popup-close">x</button>
7 + </div>
8 + @include('partials/entity-selector', ['name' => 'entity-selector'])
9 + <div class="popup-footer">
10 + <button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">Select</button>
11 + </div>
12 + </div>
13 + </div>
14 +</div>
...\ No newline at end of file ...\ No newline at end of file
...@@ -76,6 +76,14 @@ class EntitySearchTest extends TestCase ...@@ -76,6 +76,14 @@ class EntitySearchTest extends TestCase
76 ->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name); 76 ->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
77 } 77 }
78 78
79 + public function test_search_quote_term_preparation()
80 + {
81 + $termString = '"192" cat "dog hat"';
82 + $repo = $this->app[\BookStack\Repos\EntityRepo::class];
83 + $preparedTerms = $repo->prepareSearchTerms($termString);
84 + $this->assertTrue($preparedTerms === ['"192"','"dog hat"', 'cat']);
85 + }
86 +
79 public function test_books_search_listing() 87 public function test_books_search_listing()
80 { 88 {
81 $book = \BookStack\Book::all()->last(); 89 $book = \BookStack\Book::all()->last();
......
...@@ -218,13 +218,24 @@ class EntityTest extends TestCase ...@@ -218,13 +218,24 @@ class EntityTest extends TestCase
218 218
219 public function test_old_page_slugs_redirect_to_new_pages() 219 public function test_old_page_slugs_redirect_to_new_pages()
220 { 220 {
221 - $page = \BookStack\Page::all()->first(); 221 + $page = \BookStack\Page::first();
222 $pageUrl = $page->getUrl(); 222 $pageUrl = $page->getUrl();
223 $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page'; 223 $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
224 + // Need to save twice since revisions are not generated in seeder.
224 $this->asAdmin()->visit($pageUrl) 225 $this->asAdmin()->visit($pageUrl)
225 ->clickInElement('#content', 'Edit') 226 ->clickInElement('#content', 'Edit')
227 + ->type('super test', '#name')
228 + ->press('Save Page');
229 +
230 + $page = \BookStack\Page::first();
231 + $pageUrl = $page->getUrl();
232 +
233 + // Second Save
234 + $this->visit($pageUrl)
235 + ->clickInElement('#content', 'Edit')
226 ->type('super test page', '#name') 236 ->type('super test page', '#name')
227 ->press('Save Page') 237 ->press('Save Page')
238 + // Check redirect
228 ->seePageIs($newPageUrl) 239 ->seePageIs($newPageUrl)
229 ->visit($pageUrl) 240 ->visit($pageUrl)
230 ->seePageIs($newPageUrl); 241 ->seePageIs($newPageUrl);
......