Dan Brown

Merge branch 'master' into release

Showing 44 changed files with 657 additions and 238 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 {
......
...@@ -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);
196 +
197 + // Update models only if there's a change in parent chain or ordering.
198 + if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
192 $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model); 199 $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
193 - $model->priority = $index; 200 + $model->priority = $priority;
194 - if ($isPage) { 201 + if ($isPage) $model->chapter_id = $chapterId;
195 - $model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
196 - }
197 $model->save(); 202 $model->save();
203 + $updatedModels->push($model);
204 + }
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
......
...@@ -197,9 +197,10 @@ class ChapterRepo extends EntityRepo ...@@ -197,9 +197,10 @@ class ChapterRepo extends EntityRepo
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 +
218 + // Update permissions if applicable
219 + if ($rebuildPermissions) {
217 $chapter->load('book'); 220 $chapter->load('book');
218 $this->permissionService->buildJointPermissionsForEntity($chapter->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,10 +259,16 @@ class ImageService ...@@ -257,10 +259,16 @@ 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');
266 + if (strpos($storageDetails['bucket'], '.') === false) {
267 + $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
268 + } else {
262 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; 269 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
263 } 270 }
271 + }
264 272
265 $this->storageUrl = $storageUrl; 273 $this->storageUrl = $storageUrl;
266 } 274 }
......
...@@ -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 - /**
7 * Get the path to a versioned file. 6 * Get the path to a versioned file.
8 * 7 *
9 * @param string $file 8 * @param string $file
10 * @return string 9 * @return string
11 - * 10 + * @throws Exception
12 - * @throws \InvalidArgumentException
13 */ 11 */
14 - function versioned_asset($file) 12 +function versioned_asset($file = '')
15 - { 13 +{
16 - static $manifest = null; 14 + // Don't require css and JS assets for testing
15 + if (config('app.env') === 'testing') return '';
17 16
18 - if (is_null($manifest)) { 17 + static $manifest = null;
19 - $manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true); 18 + $manifestPath = 'build/manifest.json';
19 +
20 + if (is_null($manifest) && file_exists($manifestPath)) {
21 + $manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
22 + } else if (!file_exists($manifestPath)) {
23 + if (config('app.env') !== 'production') {
24 + $path = public_path($manifestPath);
25 + $error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
26 + } else {
27 + $error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
28 + }
29 + throw new \Exception($error);
20 } 30 }
21 31
22 if (isset($manifest[$file])) { 32 if (isset($manifest[$file])) {
23 return baseUrl($manifest[$file]); 33 return baseUrl($manifest[$file]);
24 } 34 }
25 35
26 - if (file_exists(public_path($file))) {
27 - return baseUrl($file);
28 - }
29 -
30 throw new InvalidArgumentException("File {$file} not defined in asset manifest."); 36 throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
31 - }
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) {
105 +
106 + if (type === 'file') {
107 + window.showEntityLinkSelector(function(entity) {
108 + let originalField = win.document.getElementById(field_name);
109 + originalField.value = entity.link;
110 + $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
111 + });
112 + }
113 +
114 + if (type === 'image') {
115 + // Show image manager
41 window.ImageManager.showExternal(function (image) { 116 window.ImageManager.showExternal(function (image) {
117 +
118 + // Set popover link input to image url then fire change event
119 + // to ensure the new value sticks
42 win.document.getElementById(field_name).value = image.url; 120 win.document.getElementById(field_name).value = image.url;
43 if ("createEvent" in document) { 121 if ("createEvent" in document) {
44 - var evt = document.createEvent("HTMLEvents"); 122 + let evt = document.createEvent("HTMLEvents");
45 evt.initEvent("change", false, true); 123 evt.initEvent("change", false, true);
46 win.document.getElementById(field_name).dispatchEvent(evt); 124 win.document.getElementById(field_name).dispatchEvent(evt);
47 } else { 125 } else {
48 win.document.getElementById(field_name).fireEvent("onchange"); 126 win.document.getElementById(field_name).fireEvent("onchange");
49 } 127 }
50 - var html = '<a href="' + image.url + '" target="_blank">'; 128 +
51 - html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">'; 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}">`;
52 html += '</a>'; 132 html += '</a>';
53 win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); 133 win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
54 }); 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,13 +162,12 @@ var mceOptions = module.exports = { ...@@ -76,13 +162,12 @@ 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 } 170 }
85 - }
86 }); 171 });
87 172
88 editor.on('drop', function (event) { 173 editor.on('drop', function (event) {
...@@ -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,13 +23,24 @@ ...@@ -22,13 +23,24 @@
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>
32 <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button> 44 <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>
...@@ -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 - </div> 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>
12 </div> 20 </div>
13 - <div class="col-md-6 faded">
14 </div> 21 </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>
51 + <td>{{ $revision->summary }}</td>
52 + @if ($index !== 0)
42 <td> 53 <td>
43 <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a> 54 <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
44 <span class="text-muted">&nbsp;|&nbsp;</span> 55 <span class="text-muted">&nbsp;|&nbsp;</span>
45 - <a href="{{ $revision->getUrl('/restore') }}">Restore</a> 56 + <a href="{{ $revision->getUrl() }}/restore">Restore</a>
46 </td> 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
1 <div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}"> 1 <div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}">
2 - <div class="overlay anim-slide" ng-show="showing" ng-cloak ng-click="hide()"> 2 + <div class="overlay" ng-cloak ng-click="hide()">
3 - <div class="image-manager-body" ng-click="$event.stopPropagation()"> 3 + <div class="popup-body" ng-click="$event.stopPropagation()">
4 +
5 + <div class="popup-header primary-background">
6 + <div class="popup-title">Image Select</div>
7 + <button class="popup-close neg corner-button button">x</button>
8 + </div>
9 +
10 + <div class="flex-fill image-manager-body">
4 11
5 <div class="image-manager-content"> 12 <div class="image-manager-content">
6 <div ng-if="imageType === 'gallery'" class="container"> 13 <div ng-if="imageType === 'gallery'" class="container">
...@@ -24,7 +31,7 @@ ...@@ -24,7 +31,7 @@
24 <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"> 31 <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
25 <div class="image-meta"> 32 <div class="image-meta">
26 <span class="name" ng-bind="image.name"></span> 33 <span class="name" ng-bind="image.name"></span>
27 - <span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span> 34 + <span class="date">Uploaded @{{ getDate(image.created_at) }}</span>
28 </div> 35 </div>
29 </div> 36 </div>
30 </div> 37 </div>
...@@ -32,14 +39,10 @@ ...@@ -32,14 +39,10 @@
32 </div> 39 </div>
33 </div> 40 </div>
34 41
35 - <button class="neg button image-manager-close" ng-click="hide()">x</button>
36 -
37 <div class="image-manager-sidebar"> 42 <div class="image-manager-sidebar">
38 - <h2>Images</h2> 43 + <div class="inner">
39 - <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
40 - <div class="image-manager-details anim fadeIn" ng-show="selectedImage">
41 44
42 - <hr class="even"> 45 + <div class="image-manager-details anim fadeIn" ng-show="selectedImage">
43 46
44 <form ng-submit="saveImageDetails($event)"> 47 <form ng-submit="saveImageDetails($event)">
45 <div> 48 <div>
...@@ -53,8 +56,6 @@ ...@@ -53,8 +56,6 @@
53 </div> 56 </div>
54 </form> 57 </form>
55 58
56 - <hr class="even">
57 -
58 <div ng-show="dependantPages"> 59 <div ng-show="dependantPages">
59 <p class="text-neg text-small"> 60 <p class="text-neg text-small">
60 This image is used in the pages below, Click delete again to confirm you want to delete 61 This image is used in the pages below, Click delete again to confirm you want to delete
...@@ -67,18 +68,27 @@ ...@@ -67,18 +68,27 @@
67 </ul> 68 </ul>
68 </div> 69 </div>
69 70
70 - <form ng-submit="deleteImage($event)"> 71 + <div class="clearfix">
71 - <button class="button neg"><i class="zmdi zmdi-delete"></i>Delete Image</button> 72 + <form class="float left" ng-submit="deleteImage($event)">
73 + <button class="button icon neg"><i class="zmdi zmdi-delete"></i></button>
72 </form> 74 </form>
73 - </div> 75 + <button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()">
74 -
75 - <div class="image-manager-bottom">
76 - <button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
77 <i class="zmdi zmdi-square-right"></i>Select Image 76 <i class="zmdi zmdi-square-right"></i>Select Image
78 </button> 77 </button>
79 </div> 78 </div>
80 79
81 </div> 80 </div>
81 +
82 + <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
83 +
84 +
85 + </div>
86 + </div>
87 +
88 +
89 +
90 + </div>
91 +
82 </div> 92 </div>
83 </div> 93 </div>
84 </div> 94 </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);
......