Showing
44 changed files
with
666 additions
and
257 deletions
.github/ISSUE_TEMPLATE.md
0 → 100644
| ... | @@ -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('/".*?"/', $term)) { |
| 171 | + $term = str_replace('"', '', $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 | }; | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -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 | } | ... | ... |
| ... | @@ -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"> » </span> | 7 | <span class="text-muted"> » </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> <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> <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 | + | | ||
| 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">»</span> | 18 | <span class="sep">»</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">»</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">»</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"> | </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"> | </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 |
This diff is collapsed.
Click to expand it.
| ... | @@ -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); | ... | ... |
-
Please register or sign in to post a comment