Showing
57 changed files
with
1049 additions
and
184 deletions
| 1 | +dist: trusty | ||
| 2 | +sudo: required | ||
| 1 | language: php | 3 | language: php |
| 2 | php: | 4 | php: |
| 3 | - 7.0 | 5 | - 7.0 |
| ... | @@ -5,15 +7,21 @@ php: | ... | @@ -5,15 +7,21 @@ php: |
| 5 | cache: | 7 | cache: |
| 6 | directories: | 8 | directories: |
| 7 | - vendor | 9 | - vendor |
| 10 | + - node_modules | ||
| 11 | + - $HOME/.composer/cache | ||
| 8 | 12 | ||
| 9 | addons: | 13 | addons: |
| 10 | - mariadb: '10.0' | 14 | + apt: |
| 15 | + packages: | ||
| 16 | + - mysql-server-5.6 | ||
| 17 | + - mysql-client-core-5.6 | ||
| 18 | + - mysql-client-5.6 | ||
| 11 | 19 | ||
| 12 | before_install: | 20 | before_install: |
| 13 | - npm install -g npm@latest | 21 | - npm install -g npm@latest |
| 14 | 22 | ||
| 15 | before_script: | 23 | before_script: |
| 16 | - - mysql -e 'create database `bookstack-test`;' | 24 | + - mysql -u root -e 'create database `bookstack-test`;' |
| 17 | - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN | 25 | - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN |
| 18 | - phpenv config-rm xdebug.ini | 26 | - phpenv config-rm xdebug.ini |
| 19 | - composer self-update | 27 | - composer self-update | ... | ... |
LICENSE
0 → 100644
| 1 | +The MIT License (MIT) | ||
| 2 | + | ||
| 3 | +Copyright (c) 2016 Dan Brown | ||
| 4 | + | ||
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | +of this software and associated documentation files (the "Software"), to deal | ||
| 7 | +in the Software without restriction, including without limitation the rights | ||
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | +copies of the Software, and to permit persons to whom the Software is | ||
| 10 | +furnished to do so, subject to the following conditions: | ||
| 11 | + | ||
| 12 | +The above copyright notice and this permission notice shall be included in all | ||
| 13 | +copies or substantial portions of the Software. | ||
| 14 | + | ||
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | +SOFTWARE. |
| ... | @@ -44,7 +44,7 @@ class Activity extends Model | ... | @@ -44,7 +44,7 @@ class Activity extends Model |
| 44 | * @return bool | 44 | * @return bool |
| 45 | */ | 45 | */ |
| 46 | public function isSimilarTo($activityB) { | 46 | public function isSimilarTo($activityB) { |
| 47 | - return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id]; | 47 | + return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id]; |
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | } | 50 | } | ... | ... |
| ... | @@ -59,7 +59,7 @@ class ChapterController extends Controller | ... | @@ -59,7 +59,7 @@ class ChapterController extends Controller |
| 59 | 59 | ||
| 60 | $input = $request->all(); | 60 | $input = $request->all(); |
| 61 | $input['priority'] = $this->bookRepo->getNewPriority($book); | 61 | $input['priority'] = $this->bookRepo->getNewPriority($book); |
| 62 | - $chapter = $this->chapterRepo->createFromInput($request->all(), $book); | 62 | + $chapter = $this->chapterRepo->createFromInput($input, $book); |
| 63 | Activity::add($chapter, 'chapter_create', $book->id); | 63 | Activity::add($chapter, 'chapter_create', $book->id); |
| 64 | return redirect($chapter->getUrl()); | 64 | return redirect($chapter->getUrl()); |
| 65 | } | 65 | } |
| ... | @@ -155,6 +155,63 @@ class ChapterController extends Controller | ... | @@ -155,6 +155,63 @@ class ChapterController extends Controller |
| 155 | } | 155 | } |
| 156 | 156 | ||
| 157 | /** | 157 | /** |
| 158 | + * Show the page for moving a chapter. | ||
| 159 | + * @param $bookSlug | ||
| 160 | + * @param $chapterSlug | ||
| 161 | + * @return mixed | ||
| 162 | + * @throws \BookStack\Exceptions\NotFoundException | ||
| 163 | + */ | ||
| 164 | + public function showMove($bookSlug, $chapterSlug) { | ||
| 165 | + $book = $this->bookRepo->getBySlug($bookSlug); | ||
| 166 | + $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); | ||
| 167 | + $this->checkOwnablePermission('chapter-update', $chapter); | ||
| 168 | + return view('chapters/move', [ | ||
| 169 | + 'chapter' => $chapter, | ||
| 170 | + 'book' => $book | ||
| 171 | + ]); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + /** | ||
| 175 | + * Perform the move action for a chapter. | ||
| 176 | + * @param $bookSlug | ||
| 177 | + * @param $chapterSlug | ||
| 178 | + * @param Request $request | ||
| 179 | + * @return mixed | ||
| 180 | + * @throws \BookStack\Exceptions\NotFoundException | ||
| 181 | + */ | ||
| 182 | + public function move($bookSlug, $chapterSlug, Request $request) { | ||
| 183 | + $book = $this->bookRepo->getBySlug($bookSlug); | ||
| 184 | + $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); | ||
| 185 | + $this->checkOwnablePermission('chapter-update', $chapter); | ||
| 186 | + | ||
| 187 | + $entitySelection = $request->get('entity_selection', null); | ||
| 188 | + if ($entitySelection === null || $entitySelection === '') { | ||
| 189 | + return redirect($chapter->getUrl()); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + $stringExploded = explode(':', $entitySelection); | ||
| 193 | + $entityType = $stringExploded[0]; | ||
| 194 | + $entityId = intval($stringExploded[1]); | ||
| 195 | + | ||
| 196 | + $parent = false; | ||
| 197 | + | ||
| 198 | + if ($entityType == 'book') { | ||
| 199 | + $parent = $this->bookRepo->getById($entityId); | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + if ($parent === false || $parent === null) { | ||
| 203 | + session()->flash('The selected Book was not found'); | ||
| 204 | + return redirect()->back(); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + $this->chapterRepo->changeBook($parent->id, $chapter); | ||
| 208 | + Activity::add($chapter, 'chapter_move', $chapter->book->id); | ||
| 209 | + session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name)); | ||
| 210 | + | ||
| 211 | + return redirect($chapter->getUrl()); | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + /** | ||
| 158 | * Show the Restrictions view. | 215 | * Show the Restrictions view. |
| 159 | * @param $bookSlug | 216 | * @param $bookSlug |
| 160 | * @param $chapterSlug | 217 | * @param $chapterSlug | ... | ... |
| ... | @@ -51,9 +51,9 @@ class ImageController extends Controller | ... | @@ -51,9 +51,9 @@ class ImageController extends Controller |
| 51 | $this->validate($request, [ | 51 | $this->validate($request, [ |
| 52 | 'term' => 'required|string' | 52 | 'term' => 'required|string' |
| 53 | ]); | 53 | ]); |
| 54 | - | 54 | + |
| 55 | $searchTerm = $request->get('term'); | 55 | $searchTerm = $request->get('term'); |
| 56 | - $imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm); | 56 | + $imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm); |
| 57 | return response()->json($imgData); | 57 | return response()->json($imgData); |
| 58 | } | 58 | } |
| 59 | 59 | ||
| ... | @@ -99,7 +99,7 @@ class ImageController extends Controller | ... | @@ -99,7 +99,7 @@ class ImageController extends Controller |
| 99 | { | 99 | { |
| 100 | $this->checkPermission('image-create-all'); | 100 | $this->checkPermission('image-create-all'); |
| 101 | $this->validate($request, [ | 101 | $this->validate($request, [ |
| 102 | - 'file' => 'image|mimes:jpeg,gif,png' | 102 | + 'file' => 'is_image' |
| 103 | ]); | 103 | ]); |
| 104 | 104 | ||
| 105 | $imageUpload = $request->file('file'); | 105 | $imageUpload = $request->file('file'); | ... | ... |
| ... | @@ -92,7 +92,7 @@ class PageController extends Controller | ... | @@ -92,7 +92,7 @@ class PageController extends Controller |
| 92 | 92 | ||
| 93 | $draftPage = $this->pageRepo->getById($pageId, true); | 93 | $draftPage = $this->pageRepo->getById($pageId, true); |
| 94 | 94 | ||
| 95 | - $chapterId = $draftPage->chapter_id; | 95 | + $chapterId = intval($draftPage->chapter_id); |
| 96 | $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book; | 96 | $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book; |
| 97 | $this->checkOwnablePermission('page-create', $parent); | 97 | $this->checkOwnablePermission('page-create', $parent); |
| 98 | 98 | ||
| ... | @@ -221,8 +221,8 @@ class PageController extends Controller | ... | @@ -221,8 +221,8 @@ class PageController extends Controller |
| 221 | $updateTime = $draft->updated_at->timestamp; | 221 | $updateTime = $draft->updated_at->timestamp; |
| 222 | $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset; | 222 | $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset; |
| 223 | return response()->json([ | 223 | return response()->json([ |
| 224 | - 'status' => 'success', | 224 | + 'status' => 'success', |
| 225 | - 'message' => 'Draft saved at ', | 225 | + 'message' => 'Draft saved at ', |
| 226 | 'timestamp' => $utcUpdateTimestamp | 226 | 'timestamp' => $utcUpdateTimestamp |
| 227 | ]); | 227 | ]); |
| 228 | } | 228 | } |
| ... | @@ -451,6 +451,67 @@ class PageController extends Controller | ... | @@ -451,6 +451,67 @@ class PageController extends Controller |
| 451 | } | 451 | } |
| 452 | 452 | ||
| 453 | /** | 453 | /** |
| 454 | + * Show the view to choose a new parent to move a page into. | ||
| 455 | + * @param $bookSlug | ||
| 456 | + * @param $pageSlug | ||
| 457 | + * @return mixed | ||
| 458 | + * @throws NotFoundException | ||
| 459 | + */ | ||
| 460 | + public function showMove($bookSlug, $pageSlug) | ||
| 461 | + { | ||
| 462 | + $book = $this->bookRepo->getBySlug($bookSlug); | ||
| 463 | + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | ||
| 464 | + $this->checkOwnablePermission('page-update', $page); | ||
| 465 | + return view('pages/move', [ | ||
| 466 | + 'book' => $book, | ||
| 467 | + 'page' => $page | ||
| 468 | + ]); | ||
| 469 | + } | ||
| 470 | + | ||
| 471 | + /** | ||
| 472 | + * Does the action of moving the location of a page | ||
| 473 | + * @param $bookSlug | ||
| 474 | + * @param $pageSlug | ||
| 475 | + * @param Request $request | ||
| 476 | + * @return mixed | ||
| 477 | + * @throws NotFoundException | ||
| 478 | + */ | ||
| 479 | + public function move($bookSlug, $pageSlug, Request $request) | ||
| 480 | + { | ||
| 481 | + $book = $this->bookRepo->getBySlug($bookSlug); | ||
| 482 | + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | ||
| 483 | + $this->checkOwnablePermission('page-update', $page); | ||
| 484 | + | ||
| 485 | + $entitySelection = $request->get('entity_selection', null); | ||
| 486 | + if ($entitySelection === null || $entitySelection === '') { | ||
| 487 | + return redirect($page->getUrl()); | ||
| 488 | + } | ||
| 489 | + | ||
| 490 | + $stringExploded = explode(':', $entitySelection); | ||
| 491 | + $entityType = $stringExploded[0]; | ||
| 492 | + $entityId = intval($stringExploded[1]); | ||
| 493 | + | ||
| 494 | + $parent = false; | ||
| 495 | + | ||
| 496 | + if ($entityType == 'chapter') { | ||
| 497 | + $parent = $this->chapterRepo->getById($entityId); | ||
| 498 | + } else if ($entityType == 'book') { | ||
| 499 | + $parent = $this->bookRepo->getById($entityId); | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + if ($parent === false || $parent === null) { | ||
| 503 | + session()->flash('The selected Book or Chapter was not found'); | ||
| 504 | + return redirect()->back(); | ||
| 505 | + } | ||
| 506 | + | ||
| 507 | + $this->pageRepo->changePageParent($page, $parent); | ||
| 508 | + Activity::add($page, 'page_move', $page->book->id); | ||
| 509 | + session()->flash('success', sprintf('Page moved to "%s"', $parent->name)); | ||
| 510 | + | ||
| 511 | + return redirect($page->getUrl()); | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + /** | ||
| 454 | * Set the permissions for this page. | 515 | * Set the permissions for this page. |
| 455 | * @param $bookSlug | 516 | * @param $bookSlug |
| 456 | * @param $pageSlug | 517 | * @param $pageSlug | ... | ... |
| ... | @@ -2,10 +2,10 @@ | ... | @@ -2,10 +2,10 @@ |
| 2 | 2 | ||
| 3 | namespace BookStack\Http\Controllers; | 3 | namespace BookStack\Http\Controllers; |
| 4 | 4 | ||
| 5 | +use BookStack\Services\ViewService; | ||
| 5 | use Illuminate\Http\Request; | 6 | use Illuminate\Http\Request; |
| 6 | 7 | ||
| 7 | use BookStack\Http\Requests; | 8 | use BookStack\Http\Requests; |
| 8 | -use BookStack\Http\Controllers\Controller; | ||
| 9 | use BookStack\Repos\BookRepo; | 9 | use BookStack\Repos\BookRepo; |
| 10 | use BookStack\Repos\ChapterRepo; | 10 | use BookStack\Repos\ChapterRepo; |
| 11 | use BookStack\Repos\PageRepo; | 11 | use BookStack\Repos\PageRepo; |
| ... | @@ -15,18 +15,21 @@ class SearchController extends Controller | ... | @@ -15,18 +15,21 @@ class SearchController extends Controller |
| 15 | protected $pageRepo; | 15 | protected $pageRepo; |
| 16 | protected $bookRepo; | 16 | protected $bookRepo; |
| 17 | protected $chapterRepo; | 17 | protected $chapterRepo; |
| 18 | + protected $viewService; | ||
| 18 | 19 | ||
| 19 | /** | 20 | /** |
| 20 | * SearchController constructor. | 21 | * SearchController constructor. |
| 21 | - * @param $pageRepo | 22 | + * @param PageRepo $pageRepo |
| 22 | - * @param $bookRepo | 23 | + * @param BookRepo $bookRepo |
| 23 | - * @param $chapterRepo | 24 | + * @param ChapterRepo $chapterRepo |
| 25 | + * @param ViewService $viewService | ||
| 24 | */ | 26 | */ |
| 25 | - public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo) | 27 | + public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService) |
| 26 | { | 28 | { |
| 27 | $this->pageRepo = $pageRepo; | 29 | $this->pageRepo = $pageRepo; |
| 28 | $this->bookRepo = $bookRepo; | 30 | $this->bookRepo = $bookRepo; |
| 29 | $this->chapterRepo = $chapterRepo; | 31 | $this->chapterRepo = $chapterRepo; |
| 32 | + $this->viewService = $viewService; | ||
| 30 | parent::__construct(); | 33 | parent::__construct(); |
| 31 | } | 34 | } |
| 32 | 35 | ||
| ... | @@ -48,9 +51,9 @@ class SearchController extends Controller | ... | @@ -48,9 +51,9 @@ class SearchController extends Controller |
| 48 | $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends); | 51 | $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends); |
| 49 | $this->setPageTitle('Search For ' . $searchTerm); | 52 | $this->setPageTitle('Search For ' . $searchTerm); |
| 50 | return view('search/all', [ | 53 | return view('search/all', [ |
| 51 | - 'pages' => $pages, | 54 | + 'pages' => $pages, |
| 52 | - 'books' => $books, | 55 | + 'books' => $books, |
| 53 | - 'chapters' => $chapters, | 56 | + 'chapters' => $chapters, |
| 54 | 'searchTerm' => $searchTerm | 57 | 'searchTerm' => $searchTerm |
| 55 | ]); | 58 | ]); |
| 56 | } | 59 | } |
| ... | @@ -69,8 +72,8 @@ class SearchController extends Controller | ... | @@ -69,8 +72,8 @@ class SearchController extends Controller |
| 69 | $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); | 72 | $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); |
| 70 | $this->setPageTitle('Page Search For ' . $searchTerm); | 73 | $this->setPageTitle('Page Search For ' . $searchTerm); |
| 71 | return view('search/entity-search-list', [ | 74 | return view('search/entity-search-list', [ |
| 72 | - 'entities' => $pages, | 75 | + 'entities' => $pages, |
| 73 | - 'title' => 'Page Search Results', | 76 | + 'title' => 'Page Search Results', |
| 74 | 'searchTerm' => $searchTerm | 77 | 'searchTerm' => $searchTerm |
| 75 | ]); | 78 | ]); |
| 76 | } | 79 | } |
| ... | @@ -89,8 +92,8 @@ class SearchController extends Controller | ... | @@ -89,8 +92,8 @@ class SearchController extends Controller |
| 89 | $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends); | 92 | $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends); |
| 90 | $this->setPageTitle('Chapter Search For ' . $searchTerm); | 93 | $this->setPageTitle('Chapter Search For ' . $searchTerm); |
| 91 | return view('search/entity-search-list', [ | 94 | return view('search/entity-search-list', [ |
| 92 | - 'entities' => $chapters, | 95 | + 'entities' => $chapters, |
| 93 | - 'title' => 'Chapter Search Results', | 96 | + 'title' => 'Chapter Search Results', |
| 94 | 'searchTerm' => $searchTerm | 97 | 'searchTerm' => $searchTerm |
| 95 | ]); | 98 | ]); |
| 96 | } | 99 | } |
| ... | @@ -109,8 +112,8 @@ class SearchController extends Controller | ... | @@ -109,8 +112,8 @@ class SearchController extends Controller |
| 109 | $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends); | 112 | $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends); |
| 110 | $this->setPageTitle('Book Search For ' . $searchTerm); | 113 | $this->setPageTitle('Book Search For ' . $searchTerm); |
| 111 | return view('search/entity-search-list', [ | 114 | return view('search/entity-search-list', [ |
| 112 | - 'entities' => $books, | 115 | + 'entities' => $books, |
| 113 | - 'title' => 'Book Search Results', | 116 | + 'title' => 'Book Search Results', |
| 114 | 'searchTerm' => $searchTerm | 117 | 'searchTerm' => $searchTerm |
| 115 | ]); | 118 | ]); |
| 116 | } | 119 | } |
| ... | @@ -134,4 +137,35 @@ class SearchController extends Controller | ... | @@ -134,4 +137,35 @@ class SearchController extends Controller |
| 134 | return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); | 137 | return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); |
| 135 | } | 138 | } |
| 136 | 139 | ||
| 140 | + | ||
| 141 | + /** | ||
| 142 | + * Search for a list of entities and return a partial HTML response of matching entities. | ||
| 143 | + * Returns the most popular entities if no search is provided. | ||
| 144 | + * @param Request $request | ||
| 145 | + * @return mixed | ||
| 146 | + */ | ||
| 147 | + public function searchEntitiesAjax(Request $request) | ||
| 148 | + { | ||
| 149 | + $entities = collect(); | ||
| 150 | + $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); | ||
| 151 | + $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false; | ||
| 152 | + | ||
| 153 | + // Search for entities otherwise show most popular | ||
| 154 | + if ($searchTerm !== false) { | ||
| 155 | + if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items()); | ||
| 156 | + if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items()); | ||
| 157 | + if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items()); | ||
| 158 | + $entities = $entities->sortByDesc('title_relevance'); | ||
| 159 | + } else { | ||
| 160 | + $entityNames = $entityTypes->map(function ($type) { | ||
| 161 | + return 'BookStack\\' . ucfirst($type); | ||
| 162 | + })->toArray(); | ||
| 163 | + $entities = $this->viewService->getPopular(20, 0, $entityNames); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + return view('search/entity-ajax-list', ['entities' => $entities]); | ||
| 167 | + } | ||
| 168 | + | ||
| 137 | } | 169 | } |
| 170 | + | ||
| 171 | + | ... | ... |
| ... | @@ -55,7 +55,7 @@ class TagController extends Controller | ... | @@ -55,7 +55,7 @@ class TagController extends Controller |
| 55 | */ | 55 | */ |
| 56 | public function getNameSuggestions(Request $request) | 56 | public function getNameSuggestions(Request $request) |
| 57 | { | 57 | { |
| 58 | - $searchTerm = $request->get('search'); | 58 | + $searchTerm = $request->has('search') ? $request->get('search') : false; |
| 59 | $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); | 59 | $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); |
| 60 | return response()->json($suggestions); | 60 | return response()->json($suggestions); |
| 61 | } | 61 | } |
| ... | @@ -66,8 +66,9 @@ class TagController extends Controller | ... | @@ -66,8 +66,9 @@ class TagController extends Controller |
| 66 | */ | 66 | */ |
| 67 | public function getValueSuggestions(Request $request) | 67 | public function getValueSuggestions(Request $request) |
| 68 | { | 68 | { |
| 69 | - $searchTerm = $request->get('search'); | 69 | + $searchTerm = $request->has('search') ? $request->get('search') : false; |
| 70 | - $suggestions = $this->tagRepo->getValueSuggestions($searchTerm); | 70 | + $tagName = $request->has('name') ? $request->get('name') : false; |
| 71 | + $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); | ||
| 71 | return response()->json($suggestions); | 72 | return response()->json($suggestions); |
| 72 | } | 73 | } |
| 73 | 74 | ... | ... |
| ... | @@ -34,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -34,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () { |
| 34 | Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); | 34 | Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); |
| 35 | Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); | 35 | Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); |
| 36 | Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); | 36 | Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); |
| 37 | + Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove'); | ||
| 38 | + Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move'); | ||
| 37 | Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); | 39 | Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); |
| 38 | Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft'); | 40 | Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft'); |
| 39 | Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict'); | 41 | Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict'); |
| ... | @@ -53,6 +55,8 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -53,6 +55,8 @@ Route::group(['middleware' => 'auth'], function () { |
| 53 | Route::post('/{bookSlug}/chapter/create', 'ChapterController@store'); | 55 | Route::post('/{bookSlug}/chapter/create', 'ChapterController@store'); |
| 54 | Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); | 56 | Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); |
| 55 | Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update'); | 57 | Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update'); |
| 58 | + Route::get('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@showMove'); | ||
| 59 | + Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move'); | ||
| 56 | Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit'); | 60 | Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit'); |
| 57 | Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict'); | 61 | Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict'); |
| 58 | Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict'); | 62 | Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict'); |
| ... | @@ -93,6 +97,8 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -93,6 +97,8 @@ Route::group(['middleware' => 'auth'], function () { |
| 93 | Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity'); | 97 | Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity'); |
| 94 | }); | 98 | }); |
| 95 | 99 | ||
| 100 | + Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax'); | ||
| 101 | + | ||
| 96 | // Links | 102 | // Links |
| 97 | Route::get('/link/{id}', 'PageController@redirectFromLink'); | 103 | Route::get('/link/{id}', 'PageController@redirectFromLink'); |
| 98 | 104 | ... | ... |
| ... | @@ -15,7 +15,12 @@ class AppServiceProvider extends ServiceProvider | ... | @@ -15,7 +15,12 @@ class AppServiceProvider extends ServiceProvider |
| 15 | */ | 15 | */ |
| 16 | public function boot() | 16 | public function boot() |
| 17 | { | 17 | { |
| 18 | - // | 18 | + // Custom validation methods |
| 19 | + \Validator::extend('is_image', function($attribute, $value, $parameters, $validator) { | ||
| 20 | + $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp']; | ||
| 21 | + return in_array($value->getMimeType(), $imageMimes); | ||
| 22 | + }); | ||
| 23 | + | ||
| 19 | } | 24 | } |
| 20 | 25 | ||
| 21 | /** | 26 | /** | ... | ... |
| ... | @@ -251,7 +251,10 @@ class BookRepo extends EntityRepo | ... | @@ -251,7 +251,10 @@ class BookRepo extends EntityRepo |
| 251 | }]); | 251 | }]); |
| 252 | $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view'); | 252 | $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view'); |
| 253 | $chapters = $chapterQuery->get(); | 253 | $chapters = $chapterQuery->get(); |
| 254 | - $children = $pages->merge($chapters); | 254 | + $children = $pages->values(); |
| 255 | + foreach ($chapters as $chapter) { | ||
| 256 | + $children->push($chapter); | ||
| 257 | + } | ||
| 255 | $bookSlug = $book->slug; | 258 | $bookSlug = $book->slug; |
| 256 | 259 | ||
| 257 | $children->each(function ($child) use ($bookSlug) { | 260 | $children->each(function ($child) use ($bookSlug) { | ... | ... |
| ... | @@ -9,6 +9,18 @@ use BookStack\Chapter; | ... | @@ -9,6 +9,18 @@ use BookStack\Chapter; |
| 9 | 9 | ||
| 10 | class ChapterRepo extends EntityRepo | 10 | class ChapterRepo extends EntityRepo |
| 11 | { | 11 | { |
| 12 | + protected $pageRepo; | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * ChapterRepo constructor. | ||
| 16 | + * @param $pageRepo | ||
| 17 | + */ | ||
| 18 | + public function __construct(PageRepo $pageRepo) | ||
| 19 | + { | ||
| 20 | + $this->pageRepo = $pageRepo; | ||
| 21 | + parent::__construct(); | ||
| 22 | + } | ||
| 23 | + | ||
| 12 | /** | 24 | /** |
| 13 | * Base query for getting chapters, Takes permissions into account. | 25 | * Base query for getting chapters, Takes permissions into account. |
| 14 | * @return mixed | 26 | * @return mixed |
| ... | @@ -189,12 +201,21 @@ class ChapterRepo extends EntityRepo | ... | @@ -189,12 +201,21 @@ class ChapterRepo extends EntityRepo |
| 189 | public function changeBook($bookId, Chapter $chapter) | 201 | public function changeBook($bookId, Chapter $chapter) |
| 190 | { | 202 | { |
| 191 | $chapter->book_id = $bookId; | 203 | $chapter->book_id = $bookId; |
| 204 | + // Update related activity | ||
| 192 | foreach ($chapter->activity as $activity) { | 205 | foreach ($chapter->activity as $activity) { |
| 193 | $activity->book_id = $bookId; | 206 | $activity->book_id = $bookId; |
| 194 | $activity->save(); | 207 | $activity->save(); |
| 195 | } | 208 | } |
| 196 | $chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id); | 209 | $chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id); |
| 197 | $chapter->save(); | 210 | $chapter->save(); |
| 211 | + // Update all child pages | ||
| 212 | + foreach ($chapter->pages as $page) { | ||
| 213 | + $this->pageRepo->changeBook($bookId, $page); | ||
| 214 | + } | ||
| 215 | + // Update permissions | ||
| 216 | + $chapter->load('book'); | ||
| 217 | + $this->permissionService->buildJointPermissionsForEntity($chapter->book); | ||
| 218 | + | ||
| 198 | return $chapter; | 219 | return $chapter; |
| 199 | } | 220 | } |
| 200 | 221 | ... | ... |
| ... | @@ -3,6 +3,7 @@ | ... | @@ -3,6 +3,7 @@ |
| 3 | use Activity; | 3 | use Activity; |
| 4 | use BookStack\Book; | 4 | use BookStack\Book; |
| 5 | use BookStack\Chapter; | 5 | use BookStack\Chapter; |
| 6 | +use BookStack\Entity; | ||
| 6 | use BookStack\Exceptions\NotFoundException; | 7 | use BookStack\Exceptions\NotFoundException; |
| 7 | use Carbon\Carbon; | 8 | use Carbon\Carbon; |
| 8 | use DOMDocument; | 9 | use DOMDocument; |
| ... | @@ -572,6 +573,22 @@ class PageRepo extends EntityRepo | ... | @@ -572,6 +573,22 @@ class PageRepo extends EntityRepo |
| 572 | return $page; | 573 | return $page; |
| 573 | } | 574 | } |
| 574 | 575 | ||
| 576 | + | ||
| 577 | + /** | ||
| 578 | + * Change the page's parent to the given entity. | ||
| 579 | + * @param Page $page | ||
| 580 | + * @param Entity $parent | ||
| 581 | + */ | ||
| 582 | + public function changePageParent(Page $page, Entity $parent) | ||
| 583 | + { | ||
| 584 | + $book = $parent->isA('book') ? $parent : $parent->book; | ||
| 585 | + $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0; | ||
| 586 | + $page->save(); | ||
| 587 | + $page = $this->changeBook($book->id, $page); | ||
| 588 | + $page->load('book'); | ||
| 589 | + $this->permissionService->buildJointPermissionsForEntity($book); | ||
| 590 | + } | ||
| 591 | + | ||
| 575 | /** | 592 | /** |
| 576 | * Gets a suitable slug for the resource | 593 | * Gets a suitable slug for the resource |
| 577 | * @param $name | 594 | * @param $name | ... | ... |
| ... | @@ -58,29 +58,48 @@ class TagRepo | ... | @@ -58,29 +58,48 @@ class TagRepo |
| 58 | 58 | ||
| 59 | /** | 59 | /** |
| 60 | * Get tag name suggestions from scanning existing tag names. | 60 | * Get tag name suggestions from scanning existing tag names. |
| 61 | + * If no search term is given the 50 most popular tag names are provided. | ||
| 61 | * @param $searchTerm | 62 | * @param $searchTerm |
| 62 | * @return array | 63 | * @return array |
| 63 | */ | 64 | */ |
| 64 | - public function getNameSuggestions($searchTerm) | 65 | + public function getNameSuggestions($searchTerm = false) |
| 65 | { | 66 | { |
| 66 | - if ($searchTerm === '') return []; | 67 | + $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name'); |
| 67 | - $query = $this->tag->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc'); | 68 | + |
| 69 | + if ($searchTerm) { | ||
| 70 | + $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc'); | ||
| 71 | + } else { | ||
| 72 | + $query = $query->orderBy('count', 'desc')->take(50); | ||
| 73 | + } | ||
| 74 | + | ||
| 68 | $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); | 75 | $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); |
| 69 | return $query->get(['name'])->pluck('name'); | 76 | return $query->get(['name'])->pluck('name'); |
| 70 | } | 77 | } |
| 71 | 78 | ||
| 72 | /** | 79 | /** |
| 73 | * Get tag value suggestions from scanning existing tag values. | 80 | * Get tag value suggestions from scanning existing tag values. |
| 81 | + * If no search is given the 50 most popular values are provided. | ||
| 82 | + * Passing a tagName will only find values for a tags with a particular name. | ||
| 74 | * @param $searchTerm | 83 | * @param $searchTerm |
| 84 | + * @param $tagName | ||
| 75 | * @return array | 85 | * @return array |
| 76 | */ | 86 | */ |
| 77 | - public function getValueSuggestions($searchTerm) | 87 | + public function getValueSuggestions($searchTerm = false, $tagName = false) |
| 78 | { | 88 | { |
| 79 | - if ($searchTerm === '') return []; | 89 | + $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value'); |
| 80 | - $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc'); | 90 | + |
| 91 | + if ($searchTerm) { | ||
| 92 | + $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc'); | ||
| 93 | + } else { | ||
| 94 | + $query = $query->orderBy('count', 'desc')->take(50); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + if ($tagName !== false) $query = $query->where('name', '=', $tagName); | ||
| 98 | + | ||
| 81 | $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); | 99 | $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); |
| 82 | return $query->get(['value'])->pluck('value'); | 100 | return $query->get(['value'])->pluck('value'); |
| 83 | } | 101 | } |
| 102 | + | ||
| 84 | /** | 103 | /** |
| 85 | * Save an array of tags to an entity | 104 | * Save an array of tags to an entity |
| 86 | * @param Entity $entity | 105 | * @param Entity $entity | ... | ... |
| ... | @@ -90,7 +90,7 @@ class ActivityService | ... | @@ -90,7 +90,7 @@ class ActivityService |
| 90 | { | 90 | { |
| 91 | $activityList = $this->permissionService | 91 | $activityList = $this->permissionService |
| 92 | ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') | 92 | ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') |
| 93 | - ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); | 93 | + ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get(); |
| 94 | 94 | ||
| 95 | return $this->filterSimilar($activityList); | 95 | return $this->filterSimilar($activityList); |
| 96 | } | 96 | } | ... | ... |
| ... | @@ -4,6 +4,7 @@ use BookStack\Book; | ... | @@ -4,6 +4,7 @@ use BookStack\Book; |
| 4 | use BookStack\Chapter; | 4 | use BookStack\Chapter; |
| 5 | use BookStack\Entity; | 5 | use BookStack\Entity; |
| 6 | use BookStack\JointPermission; | 6 | use BookStack\JointPermission; |
| 7 | +use BookStack\Ownable; | ||
| 7 | use BookStack\Page; | 8 | use BookStack\Page; |
| 8 | use BookStack\Role; | 9 | use BookStack\Role; |
| 9 | use BookStack\User; | 10 | use BookStack\User; |
| ... | @@ -307,16 +308,16 @@ class PermissionService | ... | @@ -307,16 +308,16 @@ class PermissionService |
| 307 | 308 | ||
| 308 | /** | 309 | /** |
| 309 | * Checks if an entity has a restriction set upon it. | 310 | * Checks if an entity has a restriction set upon it. |
| 310 | - * @param Entity $entity | 311 | + * @param Ownable $ownable |
| 311 | * @param $permission | 312 | * @param $permission |
| 312 | * @return bool | 313 | * @return bool |
| 313 | */ | 314 | */ |
| 314 | - public function checkEntityUserAccess(Entity $entity, $permission) | 315 | + public function checkOwnableUserAccess(Ownable $ownable, $permission) |
| 315 | { | 316 | { |
| 316 | if ($this->isAdmin) return true; | 317 | if ($this->isAdmin) return true; |
| 317 | $explodedPermission = explode('-', $permission); | 318 | $explodedPermission = explode('-', $permission); |
| 318 | 319 | ||
| 319 | - $baseQuery = $entity->where('id', '=', $entity->id); | 320 | + $baseQuery = $ownable->where('id', '=', $ownable->id); |
| 320 | $action = end($explodedPermission); | 321 | $action = end($explodedPermission); |
| 321 | $this->currentAction = $action; | 322 | $this->currentAction = $action; |
| 322 | 323 | ||
| ... | @@ -327,7 +328,7 @@ class PermissionService | ... | @@ -327,7 +328,7 @@ class PermissionService |
| 327 | $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all'); | 328 | $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all'); |
| 328 | $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own'); | 329 | $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own'); |
| 329 | $this->currentAction = 'view'; | 330 | $this->currentAction = 'view'; |
| 330 | - $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by; | 331 | + $isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by; |
| 331 | return ($allPermission || ($isOwner && $ownPermission)); | 332 | return ($allPermission || ($isOwner && $ownPermission)); |
| 332 | } | 333 | } |
| 333 | 334 | ... | ... |
| ... | @@ -50,7 +50,7 @@ class ViewService | ... | @@ -50,7 +50,7 @@ class ViewService |
| 50 | * Get the entities with the most views. | 50 | * Get the entities with the most views. |
| 51 | * @param int $count | 51 | * @param int $count |
| 52 | * @param int $page | 52 | * @param int $page |
| 53 | - * @param bool|false $filterModel | 53 | + * @param bool|false|array $filterModel |
| 54 | */ | 54 | */ |
| 55 | public function getPopular($count = 10, $page = 0, $filterModel = false) | 55 | public function getPopular($count = 10, $page = 0, $filterModel = false) |
| 56 | { | 56 | { |
| ... | @@ -60,7 +60,11 @@ class ViewService | ... | @@ -60,7 +60,11 @@ class ViewService |
| 60 | ->groupBy('viewable_id', 'viewable_type') | 60 | ->groupBy('viewable_id', 'viewable_type') |
| 61 | ->orderBy('view_count', 'desc'); | 61 | ->orderBy('view_count', 'desc'); |
| 62 | 62 | ||
| 63 | - if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); | 63 | + if ($filterModel && is_array($filterModel)) { |
| 64 | + $query->whereIn('viewable_type', $filterModel); | ||
| 65 | + } else if ($filterModel) { | ||
| 66 | + $query->where('viewable_type', '=', get_class($filterModel)); | ||
| 67 | + }; | ||
| 64 | 68 | ||
| 65 | return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); | 69 | return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); |
| 66 | } | 70 | } | ... | ... |
| 1 | <?php | 1 | <?php |
| 2 | 2 | ||
| 3 | +use BookStack\Ownable; | ||
| 4 | + | ||
| 3 | if (!function_exists('versioned_asset')) { | 5 | if (!function_exists('versioned_asset')) { |
| 4 | /** | 6 | /** |
| 5 | * Get the path to a versioned file. | 7 | * Get the path to a versioned file. |
| ... | @@ -34,18 +36,18 @@ if (!function_exists('versioned_asset')) { | ... | @@ -34,18 +36,18 @@ if (!function_exists('versioned_asset')) { |
| 34 | * If an ownable element is passed in the jointPermissions are checked against | 36 | * If an ownable element is passed in the jointPermissions are checked against |
| 35 | * that particular item. | 37 | * that particular item. |
| 36 | * @param $permission | 38 | * @param $permission |
| 37 | - * @param \BookStack\Ownable $ownable | 39 | + * @param Ownable $ownable |
| 38 | * @return mixed | 40 | * @return mixed |
| 39 | */ | 41 | */ |
| 40 | -function userCan($permission, \BookStack\Ownable $ownable = null) | 42 | +function userCan($permission, Ownable $ownable = null) |
| 41 | { | 43 | { |
| 42 | if ($ownable === null) { | 44 | if ($ownable === null) { |
| 43 | return auth()->user() && auth()->user()->can($permission); | 45 | return auth()->user() && auth()->user()->can($permission); |
| 44 | } | 46 | } |
| 45 | 47 | ||
| 46 | // Check permission on ownable item | 48 | // Check permission on ownable item |
| 47 | - $permissionService = app('BookStack\Services\PermissionService'); | 49 | + $permissionService = app(\BookStack\Services\PermissionService::class); |
| 48 | - return $permissionService->checkEntityUserAccess($ownable, $permission); | 50 | + return $permissionService->checkOwnableUserAccess($ownable, $permission); |
| 49 | } | 51 | } |
| 50 | 52 | ||
| 51 | /** | 53 | /** | ... | ... |
| ... | @@ -5,6 +5,8 @@ | ... | @@ -5,6 +5,8 @@ |
| 5 | */ | 5 | */ |
| 6 | return [ | 6 | return [ |
| 7 | 7 | ||
| 8 | - 'app-editor' => 'wysiwyg' | 8 | + 'app-editor' => 'wysiwyg', |
| 9 | + 'app-color' => '#0288D1', | ||
| 10 | + 'app-color-light' => 'rgba(21, 101, 192, 0.15)' | ||
| 9 | 11 | ||
| 10 | ]; | 12 | ]; |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -12,7 +12,13 @@ class CreateBooksTable extends Migration | ... | @@ -12,7 +12,13 @@ class CreateBooksTable extends Migration |
| 12 | */ | 12 | */ |
| 13 | public function up() | 13 | public function up() |
| 14 | { | 14 | { |
| 15 | - Schema::create('books', function (Blueprint $table) { | 15 | + $pdo = \DB::connection()->getPdo(); |
| 16 | + $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); | ||
| 17 | + $requiresISAM = strpos($mysqlVersion, '5.5') === 0; | ||
| 18 | + | ||
| 19 | + Schema::create('books', function (Blueprint $table) use ($requiresISAM) { | ||
| 20 | + if($requiresISAM) $table->engine = 'MyISAM'; | ||
| 21 | + | ||
| 16 | $table->increments('id'); | 22 | $table->increments('id'); |
| 17 | $table->string('name'); | 23 | $table->string('name'); |
| 18 | $table->string('slug')->indexed(); | 24 | $table->string('slug')->indexed(); | ... | ... |
| ... | @@ -12,7 +12,13 @@ class CreatePagesTable extends Migration | ... | @@ -12,7 +12,13 @@ class CreatePagesTable extends Migration |
| 12 | */ | 12 | */ |
| 13 | public function up() | 13 | public function up() |
| 14 | { | 14 | { |
| 15 | - Schema::create('pages', function (Blueprint $table) { | 15 | + $pdo = \DB::connection()->getPdo(); |
| 16 | + $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); | ||
| 17 | + $requiresISAM = strpos($mysqlVersion, '5.5') === 0; | ||
| 18 | + | ||
| 19 | + Schema::create('pages', function (Blueprint $table) use ($requiresISAM) { | ||
| 20 | + if($requiresISAM) $table->engine = 'MyISAM'; | ||
| 21 | + | ||
| 16 | $table->increments('id'); | 22 | $table->increments('id'); |
| 17 | $table->integer('book_id'); | 23 | $table->integer('book_id'); |
| 18 | $table->integer('chapter_id'); | 24 | $table->integer('chapter_id'); | ... | ... |
| ... | @@ -12,7 +12,12 @@ class CreateChaptersTable extends Migration | ... | @@ -12,7 +12,12 @@ class CreateChaptersTable extends Migration |
| 12 | */ | 12 | */ |
| 13 | public function up() | 13 | public function up() |
| 14 | { | 14 | { |
| 15 | - Schema::create('chapters', function (Blueprint $table) { | 15 | + $pdo = \DB::connection()->getPdo(); |
| 16 | + $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); | ||
| 17 | + $requiresISAM = strpos($mysqlVersion, '5.5') === 0; | ||
| 18 | + | ||
| 19 | + Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) { | ||
| 20 | + if($requiresISAM) $table->engine = 'MyISAM'; | ||
| 16 | $table->increments('id'); | 21 | $table->increments('id'); |
| 17 | $table->integer('book_id'); | 22 | $table->integer('book_id'); |
| 18 | $table->string('slug')->indexed(); | 23 | $table->string('slug')->indexed(); | ... | ... |
| 1 | # BookStack | 1 | # BookStack |
| 2 | 2 | ||
| 3 | +[](https://github.com/ssddanbrown/BookStack/releases/latest) | ||
| 4 | +[](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE) | ||
| 3 | [](https://travis-ci.org/ssddanbrown/BookStack) | 5 | [](https://travis-ci.org/ssddanbrown/BookStack) |
| 4 | 6 | ||
| 5 | A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. | 7 | A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. | ... | ... |
| ... | @@ -379,6 +379,15 @@ module.exports = function (ngApp, events) { | ... | @@ -379,6 +379,15 @@ module.exports = function (ngApp, events) { |
| 379 | saveDraft(); | 379 | saveDraft(); |
| 380 | }; | 380 | }; |
| 381 | 381 | ||
| 382 | + // Listen to shortcuts coming via events | ||
| 383 | + $scope.$on('editor-keydown', (event, data) => { | ||
| 384 | + // Save shortcut (ctrl+s) | ||
| 385 | + if (data.keyCode == 83 && (navigator.platform.match("Mac") ? data.metaKey : data.ctrlKey)) { | ||
| 386 | + data.preventDefault(); | ||
| 387 | + saveDraft(); | ||
| 388 | + } | ||
| 389 | + }); | ||
| 390 | + | ||
| 382 | /** | 391 | /** |
| 383 | * Discard the current draft and grab the current page | 392 | * Discard the current draft and grab the current page |
| 384 | * content from the system via an AJAX request. | 393 | * content from the system via an AJAX request. | ... | ... |
| ... | @@ -149,7 +149,10 @@ module.exports = function (ngApp, events) { | ... | @@ -149,7 +149,10 @@ module.exports = function (ngApp, events) { |
| 149 | }; | 149 | }; |
| 150 | }]); | 150 | }]); |
| 151 | 151 | ||
| 152 | - | 152 | + /** |
| 153 | + * Dropdown | ||
| 154 | + * Provides some simple logic to create small dropdown menus | ||
| 155 | + */ | ||
| 153 | ngApp.directive('dropdown', [function () { | 156 | ngApp.directive('dropdown', [function () { |
| 154 | return { | 157 | return { |
| 155 | restrict: 'A', | 158 | restrict: 'A', |
| ... | @@ -166,7 +169,11 @@ module.exports = function (ngApp, events) { | ... | @@ -166,7 +169,11 @@ module.exports = function (ngApp, events) { |
| 166 | }; | 169 | }; |
| 167 | }]); | 170 | }]); |
| 168 | 171 | ||
| 169 | - ngApp.directive('tinymce', ['$timeout', function($timeout) { | 172 | + /** |
| 173 | + * TinyMCE | ||
| 174 | + * An angular wrapper around the tinyMCE editor. | ||
| 175 | + */ | ||
| 176 | + ngApp.directive('tinymce', ['$timeout', function ($timeout) { | ||
| 170 | return { | 177 | return { |
| 171 | restrict: 'A', | 178 | restrict: 'A', |
| 172 | scope: { | 179 | scope: { |
| ... | @@ -185,6 +192,10 @@ module.exports = function (ngApp, events) { | ... | @@ -185,6 +192,10 @@ module.exports = function (ngApp, events) { |
| 185 | scope.mceChange(content); | 192 | scope.mceChange(content); |
| 186 | }); | 193 | }); |
| 187 | 194 | ||
| 195 | + editor.on('keydown', (event) => { | ||
| 196 | + scope.$emit('editor-keydown', event); | ||
| 197 | + }); | ||
| 198 | + | ||
| 188 | editor.on('init', (e) => { | 199 | editor.on('init', (e) => { |
| 189 | scope.mceModel = editor.getContent(); | 200 | scope.mceModel = editor.getContent(); |
| 190 | }); | 201 | }); |
| ... | @@ -200,8 +211,8 @@ module.exports = function (ngApp, events) { | ... | @@ -200,8 +211,8 @@ module.exports = function (ngApp, events) { |
| 200 | scope.tinymce.extraSetups.push(tinyMceSetup); | 211 | scope.tinymce.extraSetups.push(tinyMceSetup); |
| 201 | 212 | ||
| 202 | // Custom tinyMCE plugins | 213 | // Custom tinyMCE plugins |
| 203 | - tinymce.PluginManager.add('customhr', function(editor) { | 214 | + tinymce.PluginManager.add('customhr', function (editor) { |
| 204 | - editor.addCommand('InsertHorizontalRule', function() { | 215 | + editor.addCommand('InsertHorizontalRule', function () { |
| 205 | var hrElem = document.createElement('hr'); | 216 | var hrElem = document.createElement('hr'); |
| 206 | var cNode = editor.selection.getNode(); | 217 | var cNode = editor.selection.getNode(); |
| 207 | var parentNode = cNode.parentNode; | 218 | var parentNode = cNode.parentNode; |
| ... | @@ -227,7 +238,11 @@ module.exports = function (ngApp, events) { | ... | @@ -227,7 +238,11 @@ module.exports = function (ngApp, events) { |
| 227 | } | 238 | } |
| 228 | }]); | 239 | }]); |
| 229 | 240 | ||
| 230 | - ngApp.directive('markdownInput', ['$timeout', function($timeout) { | 241 | + /** |
| 242 | + * Markdown input | ||
| 243 | + * Handles the logic for just the editor input field. | ||
| 244 | + */ | ||
| 245 | + ngApp.directive('markdownInput', ['$timeout', function ($timeout) { | ||
| 231 | return { | 246 | return { |
| 232 | restrict: 'A', | 247 | restrict: 'A', |
| 233 | scope: { | 248 | scope: { |
| ... | @@ -251,7 +266,7 @@ module.exports = function (ngApp, events) { | ... | @@ -251,7 +266,7 @@ module.exports = function (ngApp, events) { |
| 251 | 266 | ||
| 252 | scope.$on('markdown-update', (event, value) => { | 267 | scope.$on('markdown-update', (event, value) => { |
| 253 | element.val(value); | 268 | element.val(value); |
| 254 | - scope.mdModel= value; | 269 | + scope.mdModel = value; |
| 255 | scope.mdChange(markdown(value)); | 270 | scope.mdChange(markdown(value)); |
| 256 | }); | 271 | }); |
| 257 | 272 | ||
| ... | @@ -259,23 +274,59 @@ module.exports = function (ngApp, events) { | ... | @@ -259,23 +274,59 @@ module.exports = function (ngApp, events) { |
| 259 | } | 274 | } |
| 260 | }]); | 275 | }]); |
| 261 | 276 | ||
| 262 | - ngApp.directive('markdownEditor', ['$timeout', function($timeout) { | 277 | + /** |
| 278 | + * Markdown Editor | ||
| 279 | + * Handles all functionality of the markdown editor. | ||
| 280 | + */ | ||
| 281 | + ngApp.directive('markdownEditor', ['$timeout', function ($timeout) { | ||
| 263 | return { | 282 | return { |
| 264 | restrict: 'A', | 283 | restrict: 'A', |
| 265 | link: function (scope, element, attrs) { | 284 | link: function (scope, element, attrs) { |
| 266 | 285 | ||
| 267 | // Elements | 286 | // Elements |
| 268 | - var input = element.find('textarea[markdown-input]'); | 287 | + const input = element.find('textarea[markdown-input]'); |
| 269 | - var insertImage = element.find('button[data-action="insertImage"]'); | 288 | + const display = element.find('.markdown-display').first(); |
| 289 | + const insertImage = element.find('button[data-action="insertImage"]'); | ||
| 270 | 290 | ||
| 271 | - var currentCaretPos = 0; | 291 | + let currentCaretPos = 0; |
| 272 | 292 | ||
| 273 | - input.blur((event) => { | 293 | + input.blur(event => { |
| 274 | currentCaretPos = input[0].selectionStart; | 294 | currentCaretPos = input[0].selectionStart; |
| 275 | }); | 295 | }); |
| 276 | 296 | ||
| 277 | - // Insert image shortcut | 297 | + // Scroll sync |
| 278 | - input.keydown((event) => { | 298 | + let inputScrollHeight, |
| 299 | + inputHeight, | ||
| 300 | + displayScrollHeight, | ||
| 301 | + displayHeight; | ||
| 302 | + | ||
| 303 | + function setScrollHeights() { | ||
| 304 | + inputScrollHeight = input[0].scrollHeight; | ||
| 305 | + inputHeight = input.height(); | ||
| 306 | + displayScrollHeight = display[0].scrollHeight; | ||
| 307 | + displayHeight = display.height(); | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + setTimeout(() => { | ||
| 311 | + setScrollHeights(); | ||
| 312 | + }, 200); | ||
| 313 | + window.addEventListener('resize', setScrollHeights); | ||
| 314 | + let scrollDebounceTime = 800; | ||
| 315 | + let lastScroll = 0; | ||
| 316 | + input.on('scroll', event => { | ||
| 317 | + let now = Date.now(); | ||
| 318 | + if (now - lastScroll > scrollDebounceTime) { | ||
| 319 | + setScrollHeights() | ||
| 320 | + } | ||
| 321 | + let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight)); | ||
| 322 | + let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent; | ||
| 323 | + display.scrollTop(displayScrollY); | ||
| 324 | + lastScroll = now; | ||
| 325 | + }); | ||
| 326 | + | ||
| 327 | + // Editor key-presses | ||
| 328 | + input.keydown(event => { | ||
| 329 | + // Insert image shortcut | ||
| 279 | if (event.which === 73 && event.ctrlKey && event.shiftKey) { | 330 | if (event.which === 73 && event.ctrlKey && event.shiftKey) { |
| 280 | event.preventDefault(); | 331 | event.preventDefault(); |
| 281 | var caretPos = input[0].selectionStart; | 332 | var caretPos = input[0].selectionStart; |
| ... | @@ -285,12 +336,15 @@ module.exports = function (ngApp, events) { | ... | @@ -285,12 +336,15 @@ module.exports = function (ngApp, events) { |
| 285 | input.focus(); | 336 | input.focus(); |
| 286 | input[0].selectionStart = caretPos + ("; | 337 | input[0].selectionStart = caretPos + ("; |
| 287 | input[0].selectionEnd = caretPos + ('; | 338 | input[0].selectionEnd = caretPos + ('; |
| 339 | + return; | ||
| 288 | } | 340 | } |
| 341 | + // Pass key presses to controller via event | ||
| 342 | + scope.$emit('editor-keydown', event); | ||
| 289 | }); | 343 | }); |
| 290 | 344 | ||
| 291 | // Insert image from image manager | 345 | // Insert image from image manager |
| 292 | - insertImage.click((event) => { | 346 | + insertImage.click(event => { |
| 293 | - window.ImageManager.showExternal((image) => { | 347 | + window.ImageManager.showExternal(image => { |
| 294 | var caretPos = currentCaretPos; | 348 | var caretPos = currentCaretPos; |
| 295 | var currentContent = input.val(); | 349 | var currentContent = input.val(); |
| 296 | var mdImageText = ""; | 350 | var mdImageText = ""; |
| ... | @@ -302,11 +356,16 @@ module.exports = function (ngApp, events) { | ... | @@ -302,11 +356,16 @@ module.exports = function (ngApp, events) { |
| 302 | } | 356 | } |
| 303 | } | 357 | } |
| 304 | }]); | 358 | }]); |
| 305 | - | 359 | + |
| 306 | - ngApp.directive('toolbox', [function() { | 360 | + /** |
| 361 | + * Page Editor Toolbox | ||
| 362 | + * Controls all functionality for the sliding toolbox | ||
| 363 | + * on the page edit view. | ||
| 364 | + */ | ||
| 365 | + ngApp.directive('toolbox', [function () { | ||
| 307 | return { | 366 | return { |
| 308 | restrict: 'A', | 367 | restrict: 'A', |
| 309 | - link: function(scope, elem, attrs) { | 368 | + link: function (scope, elem, attrs) { |
| 310 | 369 | ||
| 311 | // Get common elements | 370 | // Get common elements |
| 312 | const $buttons = elem.find('[tab-button]'); | 371 | const $buttons = elem.find('[tab-button]'); |
| ... | @@ -317,7 +376,7 @@ module.exports = function (ngApp, events) { | ... | @@ -317,7 +376,7 @@ module.exports = function (ngApp, events) { |
| 317 | $toggle.click((e) => { | 376 | $toggle.click((e) => { |
| 318 | elem.toggleClass('open'); | 377 | elem.toggleClass('open'); |
| 319 | }); | 378 | }); |
| 320 | - | 379 | + |
| 321 | // Set an active tab/content by name | 380 | // Set an active tab/content by name |
| 322 | function setActive(tabName, openToolbox) { | 381 | function setActive(tabName, openToolbox) { |
| 323 | $buttons.removeClass('active'); | 382 | $buttons.removeClass('active'); |
| ... | @@ -331,7 +390,7 @@ module.exports = function (ngApp, events) { | ... | @@ -331,7 +390,7 @@ module.exports = function (ngApp, events) { |
| 331 | setActive($content.first().attr('tab-content'), false); | 390 | setActive($content.first().attr('tab-content'), false); |
| 332 | 391 | ||
| 333 | // Handle tab button click | 392 | // Handle tab button click |
| 334 | - $buttons.click(function(e) { | 393 | + $buttons.click(function (e) { |
| 335 | let name = $(this).attr('tab-button'); | 394 | let name = $(this).attr('tab-button'); |
| 336 | setActive(name, true); | 395 | setActive(name, true); |
| 337 | }); | 396 | }); |
| ... | @@ -339,11 +398,16 @@ module.exports = function (ngApp, events) { | ... | @@ -339,11 +398,16 @@ module.exports = function (ngApp, events) { |
| 339 | } | 398 | } |
| 340 | }]); | 399 | }]); |
| 341 | 400 | ||
| 342 | - ngApp.directive('autosuggestions', ['$http', function($http) { | 401 | + /** |
| 402 | + * Tag Autosuggestions | ||
| 403 | + * Listens to child inputs and provides autosuggestions depending on field type | ||
| 404 | + * and input. Suggestions provided by server. | ||
| 405 | + */ | ||
| 406 | + ngApp.directive('tagAutosuggestions', ['$http', function ($http) { | ||
| 343 | return { | 407 | return { |
| 344 | restrict: 'A', | 408 | restrict: 'A', |
| 345 | - link: function(scope, elem, attrs) { | 409 | + link: function (scope, elem, attrs) { |
| 346 | - | 410 | + |
| 347 | // Local storage for quick caching. | 411 | // Local storage for quick caching. |
| 348 | const localCache = {}; | 412 | const localCache = {}; |
| 349 | 413 | ||
| ... | @@ -360,38 +424,49 @@ module.exports = function (ngApp, events) { | ... | @@ -360,38 +424,49 @@ module.exports = function (ngApp, events) { |
| 360 | let active = 0; | 424 | let active = 0; |
| 361 | 425 | ||
| 362 | // Listen to input events on autosuggest fields | 426 | // Listen to input events on autosuggest fields |
| 363 | - elem.on('input', '[autosuggest]', function(event) { | 427 | + elem.on('input focus', '[autosuggest]', function (event) { |
| 364 | let $input = $(this); | 428 | let $input = $(this); |
| 365 | let val = $input.val(); | 429 | let val = $input.val(); |
| 366 | let url = $input.attr('autosuggest'); | 430 | let url = $input.attr('autosuggest'); |
| 367 | - // No suggestions until at least 3 chars | 431 | + let type = $input.attr('autosuggest-type'); |
| 368 | - if (val.length < 3) { | 432 | + |
| 369 | - if (isShowing) { | 433 | + // Add name param to request if for a value |
| 370 | - $suggestionBox.hide(); | 434 | + if (type.toLowerCase() === 'value') { |
| 371 | - isShowing = false; | 435 | + let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); |
| 436 | + let nameVal = $nameInput.val(); | ||
| 437 | + if (nameVal !== '') { | ||
| 438 | + url += '?name=' + encodeURIComponent(nameVal); | ||
| 372 | } | 439 | } |
| 373 | - return; | 440 | + } |
| 374 | - }; | ||
| 375 | 441 | ||
| 376 | let suggestionPromise = getSuggestions(val.slice(0, 3), url); | 442 | let suggestionPromise = getSuggestions(val.slice(0, 3), url); |
| 377 | - suggestionPromise.then((suggestions) => { | 443 | + suggestionPromise.then(suggestions => { |
| 378 | - if (val.length > 2) { | 444 | + if (val.length === 0) { |
| 379 | - suggestions = suggestions.filter((item) => { | 445 | + displaySuggestions($input, suggestions.slice(0, 6)); |
| 380 | - return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; | 446 | + } else { |
| 381 | - }).slice(0, 4); | 447 | + suggestions = suggestions.filter(item => { |
| 382 | - displaySuggestions($input, suggestions); | 448 | + return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; |
| 383 | - } | 449 | + }).slice(0, 4); |
| 450 | + displaySuggestions($input, suggestions); | ||
| 451 | + } | ||
| 384 | }); | 452 | }); |
| 385 | }); | 453 | }); |
| 386 | 454 | ||
| 387 | // Hide autosuggestions when input loses focus. | 455 | // Hide autosuggestions when input loses focus. |
| 388 | // Slight delay to allow clicks. | 456 | // Slight delay to allow clicks. |
| 389 | - elem.on('blur', '[autosuggest]', function(event) { | 457 | + let lastFocusTime = 0; |
| 458 | + elem.on('blur', '[autosuggest]', function (event) { | ||
| 459 | + let startTime = Date.now(); | ||
| 390 | setTimeout(() => { | 460 | setTimeout(() => { |
| 391 | - $suggestionBox.hide(); | 461 | + if (lastFocusTime < startTime) { |
| 392 | - isShowing = false; | 462 | + $suggestionBox.hide(); |
| 463 | + isShowing = false; | ||
| 464 | + } | ||
| 393 | }, 200) | 465 | }, 200) |
| 394 | }); | 466 | }); |
| 467 | + elem.on('focus', '[autosuggest]', function (event) { | ||
| 468 | + lastFocusTime = Date.now(); | ||
| 469 | + }); | ||
| 395 | 470 | ||
| 396 | elem.on('keydown', '[autosuggest]', function (event) { | 471 | elem.on('keydown', '[autosuggest]', function (event) { |
| 397 | if (!isShowing) return; | 472 | if (!isShowing) return; |
| ... | @@ -401,23 +476,25 @@ module.exports = function (ngApp, events) { | ... | @@ -401,23 +476,25 @@ module.exports = function (ngApp, events) { |
| 401 | 476 | ||
| 402 | // Down arrow | 477 | // Down arrow |
| 403 | if (event.keyCode === 40) { | 478 | if (event.keyCode === 40) { |
| 404 | - let newActive = (active === suggestCount-1) ? 0 : active + 1; | 479 | + let newActive = (active === suggestCount - 1) ? 0 : active + 1; |
| 405 | changeActiveTo(newActive, suggestionElems); | 480 | changeActiveTo(newActive, suggestionElems); |
| 406 | } | 481 | } |
| 407 | // Up arrow | 482 | // Up arrow |
| 408 | else if (event.keyCode === 38) { | 483 | else if (event.keyCode === 38) { |
| 409 | - let newActive = (active === 0) ? suggestCount-1 : active - 1; | 484 | + let newActive = (active === 0) ? suggestCount - 1 : active - 1; |
| 410 | changeActiveTo(newActive, suggestionElems); | 485 | changeActiveTo(newActive, suggestionElems); |
| 411 | } | 486 | } |
| 412 | - // Enter key | 487 | + // Enter or tab key |
| 413 | - else if (event.keyCode === 13) { | 488 | + else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) { |
| 414 | let text = suggestionElems[active].textContent; | 489 | let text = suggestionElems[active].textContent; |
| 415 | currentInput[0].value = text; | 490 | currentInput[0].value = text; |
| 416 | currentInput.focus(); | 491 | currentInput.focus(); |
| 417 | $suggestionBox.hide(); | 492 | $suggestionBox.hide(); |
| 418 | isShowing = false; | 493 | isShowing = false; |
| 419 | - event.preventDefault(); | 494 | + if (event.keyCode === 13) { |
| 420 | - return false; | 495 | + event.preventDefault(); |
| 496 | + return false; | ||
| 497 | + } | ||
| 421 | } | 498 | } |
| 422 | }); | 499 | }); |
| 423 | 500 | ||
| ... | @@ -430,6 +507,7 @@ module.exports = function (ngApp, events) { | ... | @@ -430,6 +507,7 @@ module.exports = function (ngApp, events) { |
| 430 | 507 | ||
| 431 | // Display suggestions on a field | 508 | // Display suggestions on a field |
| 432 | let prevSuggestions = []; | 509 | let prevSuggestions = []; |
| 510 | + | ||
| 433 | function displaySuggestions($input, suggestions) { | 511 | function displaySuggestions($input, suggestions) { |
| 434 | 512 | ||
| 435 | // Hide if no suggestions | 513 | // Hide if no suggestions |
| ... | @@ -466,7 +544,8 @@ module.exports = function (ngApp, events) { | ... | @@ -466,7 +544,8 @@ module.exports = function (ngApp, events) { |
| 466 | if (i === 0) { | 544 | if (i === 0) { |
| 467 | suggestion.className = 'active' | 545 | suggestion.className = 'active' |
| 468 | active = 0; | 546 | active = 0; |
| 469 | - }; | 547 | + } |
| 548 | + ; | ||
| 470 | $suggestionBox[0].appendChild(suggestion); | 549 | $suggestionBox[0].appendChild(suggestion); |
| 471 | } | 550 | } |
| 472 | 551 | ||
| ... | @@ -484,17 +563,18 @@ module.exports = function (ngApp, events) { | ... | @@ -484,17 +563,18 @@ module.exports = function (ngApp, events) { |
| 484 | 563 | ||
| 485 | // Get suggestions & cache | 564 | // Get suggestions & cache |
| 486 | function getSuggestions(input, url) { | 565 | function getSuggestions(input, url) { |
| 487 | - let searchUrl = url + '?search=' + encodeURIComponent(input); | 566 | + let hasQuery = url.indexOf('?') !== -1; |
| 567 | + let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input); | ||
| 488 | 568 | ||
| 489 | // Get from local cache if exists | 569 | // Get from local cache if exists |
| 490 | - if (localCache[searchUrl]) { | 570 | + if (typeof localCache[searchUrl] !== 'undefined') { |
| 491 | return new Promise((resolve, reject) => { | 571 | return new Promise((resolve, reject) => { |
| 492 | - resolve(localCache[input]); | 572 | + resolve(localCache[searchUrl]); |
| 493 | }); | 573 | }); |
| 494 | } | 574 | } |
| 495 | 575 | ||
| 496 | - return $http.get(searchUrl).then((response) => { | 576 | + return $http.get(searchUrl).then(response => { |
| 497 | - localCache[input] = response.data; | 577 | + localCache[searchUrl] = response.data; |
| 498 | return response.data; | 578 | return response.data; |
| 499 | }); | 579 | }); |
| 500 | } | 580 | } |
| ... | @@ -502,6 +582,67 @@ module.exports = function (ngApp, events) { | ... | @@ -502,6 +582,67 @@ module.exports = function (ngApp, events) { |
| 502 | } | 582 | } |
| 503 | } | 583 | } |
| 504 | }]); | 584 | }]); |
| 585 | + | ||
| 586 | + | ||
| 587 | + ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) { | ||
| 588 | + return { | ||
| 589 | + restrict: 'A', | ||
| 590 | + scope: true, | ||
| 591 | + link: function (scope, element, attrs) { | ||
| 592 | + scope.loading = true; | ||
| 593 | + scope.entityResults = false; | ||
| 594 | + scope.search = ''; | ||
| 595 | + | ||
| 596 | + // Add input for forms | ||
| 597 | + const input = element.find('[entity-selector-input]').first(); | ||
| 598 | + | ||
| 599 | + // Listen to entity item clicks | ||
| 600 | + element.on('click', '.entity-list a', function(event) { | ||
| 601 | + event.preventDefault(); | ||
| 602 | + event.stopPropagation(); | ||
| 603 | + let item = $(this).closest('[data-entity-type]'); | ||
| 604 | + itemSelect(item); | ||
| 605 | + }); | ||
| 606 | + element.on('click', '[data-entity-type]', function(event) { | ||
| 607 | + itemSelect($(this)); | ||
| 608 | + }); | ||
| 609 | + | ||
| 610 | + // Select entity action | ||
| 611 | + function itemSelect(item) { | ||
| 612 | + let entityType = item.attr('data-entity-type'); | ||
| 613 | + let entityId = item.attr('data-entity-id'); | ||
| 614 | + let isSelected = !item.hasClass('selected'); | ||
| 615 | + element.find('.selected').removeClass('selected').removeClass('primary-background'); | ||
| 616 | + if (isSelected) item.addClass('selected').addClass('primary-background'); | ||
| 617 | + let newVal = isSelected ? `${entityType}:${entityId}` : ''; | ||
| 618 | + input.val(newVal); | ||
| 619 | + } | ||
| 620 | + | ||
| 621 | + // Get search url with correct types | ||
| 622 | + function getSearchUrl() { | ||
| 623 | + let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter'); | ||
| 624 | + return `/ajax/search/entities?types=${types}`; | ||
| 625 | + } | ||
| 626 | + | ||
| 627 | + // Get initial contents | ||
| 628 | + $http.get(getSearchUrl()).then(resp => { | ||
| 629 | + scope.entityResults = $sce.trustAsHtml(resp.data); | ||
| 630 | + scope.loading = false; | ||
| 631 | + }); | ||
| 632 | + | ||
| 633 | + // Search when typing | ||
| 634 | + scope.searchEntities = function() { | ||
| 635 | + scope.loading = true; | ||
| 636 | + input.val(''); | ||
| 637 | + let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search); | ||
| 638 | + $http.get(url).then(resp => { | ||
| 639 | + scope.entityResults = $sce.trustAsHtml(resp.data); | ||
| 640 | + scope.loading = false; | ||
| 641 | + }); | ||
| 642 | + }; | ||
| 643 | + } | ||
| 644 | + }; | ||
| 645 | + }]); | ||
| 505 | }; | 646 | }; |
| 506 | 647 | ||
| 507 | 648 | ... | ... |
| ... | @@ -112,16 +112,11 @@ $(function () { | ... | @@ -112,16 +112,11 @@ $(function () { |
| 112 | 112 | ||
| 113 | // Common jQuery actions | 113 | // Common jQuery actions |
| 114 | $('[data-action="expand-entity-list-details"]').click(function() { | 114 | $('[data-action="expand-entity-list-details"]').click(function() { |
| 115 | - $('.entity-list.compact').find('p').slideToggle(240); | 115 | + $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240); |
| 116 | }); | 116 | }); |
| 117 | 117 | ||
| 118 | 118 | ||
| 119 | }); | 119 | }); |
| 120 | 120 | ||
| 121 | - | ||
| 122 | -function elemExists(selector) { | ||
| 123 | - return document.querySelector(selector) !== null; | ||
| 124 | -} | ||
| 125 | - | ||
| 126 | // Page specific items | 121 | // Page specific items |
| 127 | require('./pages/page-show'); | 122 | require('./pages/page-show'); | ... | ... |
| 1 | var mceOptions = module.exports = { | 1 | var mceOptions = module.exports = { |
| 2 | selector: '#html-editor', | 2 | selector: '#html-editor', |
| 3 | content_css: [ | 3 | content_css: [ |
| 4 | - '/css/styles.css' | 4 | + '/css/styles.css', |
| 5 | + '/libs/material-design-iconic-font/css/material-design-iconic-font.min.css' | ||
| 5 | ], | 6 | ], |
| 6 | body_class: 'page-content', | 7 | body_class: 'page-content', |
| 7 | relative_urls: false, | 8 | relative_urls: false, |
| ... | @@ -19,11 +20,18 @@ var mceOptions = module.exports = { | ... | @@ -19,11 +20,18 @@ var mceOptions = module.exports = { |
| 19 | {title: "Header 1", format: "h1"}, | 20 | {title: "Header 1", format: "h1"}, |
| 20 | {title: "Header 2", format: "h2"}, | 21 | {title: "Header 2", format: "h2"}, |
| 21 | {title: "Header 3", format: "h3"}, | 22 | {title: "Header 3", format: "h3"}, |
| 22 | - {title: "Paragraph", format: "p"}, | 23 | + {title: "Paragraph", format: "p", exact: true, classes: ''}, |
| 23 | {title: "Blockquote", format: "blockquote"}, | 24 | {title: "Blockquote", format: "blockquote"}, |
| 24 | {title: "Code Block", icon: "code", format: "pre"}, | 25 | {title: "Code Block", icon: "code", format: "pre"}, |
| 25 | - {title: "Inline Code", icon: "code", inline: "code"} | 26 | + {title: "Inline Code", icon: "code", inline: "code"}, |
| 27 | + {title: "Callouts", items: [ | ||
| 28 | + {title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}}, | ||
| 29 | + {title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}}, | ||
| 30 | + {title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}}, | ||
| 31 | + {title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}} | ||
| 32 | + ]} | ||
| 26 | ], | 33 | ], |
| 34 | + style_formats_merge: false, | ||
| 27 | formats: { | 35 | formats: { |
| 28 | alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'}, | 36 | alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'}, |
| 29 | aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'}, | 37 | aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'}, | ... | ... |
| ... | @@ -74,15 +74,15 @@ window.setupPageShow = module.exports = function (pageId) { | ... | @@ -74,15 +74,15 @@ window.setupPageShow = module.exports = function (pageId) { |
| 74 | // Make the book-tree sidebar stick in view on scroll | 74 | // Make the book-tree sidebar stick in view on scroll |
| 75 | var $window = $(window); | 75 | var $window = $(window); |
| 76 | var $bookTree = $(".book-tree"); | 76 | var $bookTree = $(".book-tree"); |
| 77 | + var $bookTreeParent = $bookTree.parent(); | ||
| 77 | // Check the page is scrollable and the content is taller than the tree | 78 | // Check the page is scrollable and the content is taller than the tree |
| 78 | var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height()); | 79 | var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height()); |
| 79 | // Get current tree's width and header height | 80 | // Get current tree's width and header height |
| 80 | var headerHeight = $("#header").height() + $(".toolbar").height(); | 81 | var headerHeight = $("#header").height() + $(".toolbar").height(); |
| 81 | var isFixed = $window.scrollTop() > headerHeight; | 82 | var isFixed = $window.scrollTop() > headerHeight; |
| 82 | - var bookTreeWidth = $bookTree.width(); | ||
| 83 | // Function to fix the tree as a sidebar | 83 | // Function to fix the tree as a sidebar |
| 84 | function stickTree() { | 84 | function stickTree() { |
| 85 | - $bookTree.width(bookTreeWidth + 48 + 15); | 85 | + $bookTree.width($bookTreeParent.width() + 15); |
| 86 | $bookTree.addClass("fixed"); | 86 | $bookTree.addClass("fixed"); |
| 87 | isFixed = true; | 87 | isFixed = true; |
| 88 | } | 88 | } |
| ... | @@ -101,13 +101,27 @@ window.setupPageShow = module.exports = function (pageId) { | ... | @@ -101,13 +101,27 @@ window.setupPageShow = module.exports = function (pageId) { |
| 101 | unstickTree(); | 101 | unstickTree(); |
| 102 | } | 102 | } |
| 103 | } | 103 | } |
| 104 | + // The event ran when the window scrolls | ||
| 105 | + function windowScrollEvent() { | ||
| 106 | + checkTreeStickiness(false); | ||
| 107 | + } | ||
| 108 | + | ||
| 104 | // If the page is scrollable and the window is wide enough listen to scroll events | 109 | // If the page is scrollable and the window is wide enough listen to scroll events |
| 105 | // and evaluate tree stickiness. | 110 | // and evaluate tree stickiness. |
| 106 | if (pageScrollable && $window.width() > 1000) { | 111 | if (pageScrollable && $window.width() > 1000) { |
| 107 | - $window.scroll(function() { | 112 | + $window.on('scroll', windowScrollEvent); |
| 108 | - checkTreeStickiness(false); | ||
| 109 | - }); | ||
| 110 | checkTreeStickiness(true); | 113 | checkTreeStickiness(true); |
| 111 | } | 114 | } |
| 112 | 115 | ||
| 116 | + // Handle window resizing and switch between desktop/mobile views | ||
| 117 | + $window.on('resize', event => { | ||
| 118 | + if (pageScrollable && $window.width() > 1000) { | ||
| 119 | + $window.on('scroll', windowScrollEvent); | ||
| 120 | + checkTreeStickiness(true); | ||
| 121 | + } else { | ||
| 122 | + $window.off('scroll', windowScrollEvent); | ||
| 123 | + unstickTree(); | ||
| 124 | + } | ||
| 125 | + }); | ||
| 126 | + | ||
| 113 | }; | 127 | }; | ... | ... |
| ... | @@ -125,3 +125,51 @@ | ... | @@ -125,3 +125,51 @@ |
| 125 | margin-right: $-xl; | 125 | margin-right: $-xl; |
| 126 | } | 126 | } |
| 127 | } | 127 | } |
| 128 | + | ||
| 129 | + | ||
| 130 | +/** | ||
| 131 | + * Callouts | ||
| 132 | + */ | ||
| 133 | + | ||
| 134 | +.callout { | ||
| 135 | + border-left: 3px solid #BBB; | ||
| 136 | + background-color: #EEE; | ||
| 137 | + padding: $-s; | ||
| 138 | + &:before { | ||
| 139 | + font-family: 'Material-Design-Iconic-Font'; | ||
| 140 | + padding-right: $-s; | ||
| 141 | + display: inline-block; | ||
| 142 | + } | ||
| 143 | + &.success { | ||
| 144 | + border-left-color: $positive; | ||
| 145 | + background-color: lighten($positive, 45%); | ||
| 146 | + color: darken($positive, 16%); | ||
| 147 | + } | ||
| 148 | + &.success:before { | ||
| 149 | + content: '\f269'; | ||
| 150 | + } | ||
| 151 | + &.danger { | ||
| 152 | + border-left-color: $negative; | ||
| 153 | + background-color: lighten($negative, 34%); | ||
| 154 | + color: darken($negative, 20%); | ||
| 155 | + } | ||
| 156 | + &.danger:before { | ||
| 157 | + content: '\f1f2'; | ||
| 158 | + } | ||
| 159 | + &.info { | ||
| 160 | + border-left-color: $info; | ||
| 161 | + background-color: lighten($info, 50%); | ||
| 162 | + color: darken($info, 16%); | ||
| 163 | + } | ||
| 164 | + &.info:before { | ||
| 165 | + content: '\f1f8'; | ||
| 166 | + } | ||
| 167 | + &.warning { | ||
| 168 | + border-left-color: $warning; | ||
| 169 | + background-color: lighten($warning, 36%); | ||
| 170 | + color: darken($warning, 16%); | ||
| 171 | + } | ||
| 172 | + &.warning:before { | ||
| 173 | + content: '\f1f1'; | ||
| 174 | + } | ||
| 175 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -20,6 +20,9 @@ | ... | @@ -20,6 +20,9 @@ |
| 20 | &.disabled, &[disabled] { | 20 | &.disabled, &[disabled] { |
| 21 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==); | 21 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==); |
| 22 | } | 22 | } |
| 23 | + &:focus { | ||
| 24 | + outline: 0; | ||
| 25 | + } | ||
| 23 | } | 26 | } |
| 24 | 27 | ||
| 25 | #html-editor { | 28 | #html-editor { | ... | ... |
| 1 | .page-list { | 1 | .page-list { |
| 2 | h3 { | 2 | h3 { |
| 3 | - margin: $-l 0 $-m 0; | 3 | + margin: $-l 0 $-xs 0; |
| 4 | + font-size: 1.666em; | ||
| 4 | } | 5 | } |
| 5 | a.chapter { | 6 | a.chapter { |
| 6 | color: $color-chapter; | 7 | color: $color-chapter; |
| ... | @@ -8,7 +9,6 @@ | ... | @@ -8,7 +9,6 @@ |
| 8 | .inset-list { | 9 | .inset-list { |
| 9 | display: none; | 10 | display: none; |
| 10 | overflow: hidden; | 11 | overflow: hidden; |
| 11 | - // padding-left: $-m; | ||
| 12 | margin-bottom: $-l; | 12 | margin-bottom: $-l; |
| 13 | } | 13 | } |
| 14 | h4 { | 14 | h4 { |
| ... | @@ -338,6 +338,10 @@ ul.pagination { | ... | @@ -338,6 +338,10 @@ ul.pagination { |
| 338 | padding-top: $-xs; | 338 | padding-top: $-xs; |
| 339 | margin: 0; | 339 | margin: 0; |
| 340 | } | 340 | } |
| 341 | + > p.empty-text { | ||
| 342 | + display: block; | ||
| 343 | + font-size: $fs-m; | ||
| 344 | + } | ||
| 341 | hr { | 345 | hr { |
| 342 | margin: 0; | 346 | margin: 0; |
| 343 | } | 347 | } | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | */ | 3 | */ |
| 4 | 4 | ||
| 5 | h1 { | 5 | h1 { |
| 6 | - font-size: 3.625em; | 6 | + font-size: 3.425em; |
| 7 | line-height: 1.22222222em; | 7 | line-height: 1.22222222em; |
| 8 | margin-top: 0.48888889em; | 8 | margin-top: 0.48888889em; |
| 9 | margin-bottom: 0.48888889em; | 9 | margin-bottom: 0.48888889em; |
| ... | @@ -33,10 +33,10 @@ h1, h2, h3, h4 { | ... | @@ -33,10 +33,10 @@ h1, h2, h3, h4 { |
| 33 | display: block; | 33 | display: block; |
| 34 | color: #555; | 34 | color: #555; |
| 35 | .subheader { | 35 | .subheader { |
| 36 | - display: block; | 36 | + //display: block; |
| 37 | font-size: 0.5em; | 37 | font-size: 0.5em; |
| 38 | line-height: 1em; | 38 | line-height: 1em; |
| 39 | - color: lighten($text-dark, 16%); | 39 | + color: lighten($text-dark, 32%); |
| 40 | } | 40 | } |
| 41 | } | 41 | } |
| 42 | 42 | ||
| ... | @@ -225,6 +225,15 @@ p.secondary, p .secondary, span.secondary, .text-secondary { | ... | @@ -225,6 +225,15 @@ p.secondary, p .secondary, span.secondary, .text-secondary { |
| 225 | color: $color-chapter; | 225 | color: $color-chapter; |
| 226 | } | 226 | } |
| 227 | } | 227 | } |
| 228 | +.faded .text-book:hover { | ||
| 229 | + color: $color-book !important; | ||
| 230 | +} | ||
| 231 | +.faded .text-chapter:hover { | ||
| 232 | + color: $color-chapter !important; | ||
| 233 | +} | ||
| 234 | +.faded .text-page:hover { | ||
| 235 | + color: $color-page !important; | ||
| 236 | +} | ||
| 228 | 237 | ||
| 229 | span.highlight { | 238 | span.highlight { |
| 230 | //background-color: rgba($primary, 0.2); | 239 | //background-color: rgba($primary, 0.2); | ... | ... |
| ... | @@ -38,6 +38,7 @@ $primary-dark: #0288D1; | ... | @@ -38,6 +38,7 @@ $primary-dark: #0288D1; |
| 38 | $secondary: #e27b41; | 38 | $secondary: #e27b41; |
| 39 | $positive: #52A256; | 39 | $positive: #52A256; |
| 40 | $negative: #E84F4F; | 40 | $negative: #E84F4F; |
| 41 | +$info: $primary; | ||
| 41 | $warning: $secondary; | 42 | $warning: $secondary; |
| 42 | $primary-faded: rgba(21, 101, 192, 0.15); | 43 | $primary-faded: rgba(21, 101, 192, 0.15); |
| 43 | 44 | ... | ... |
| ... | @@ -207,3 +207,59 @@ $btt-size: 40px; | ... | @@ -207,3 +207,59 @@ $btt-size: 40px; |
| 207 | color: #EEE; | 207 | color: #EEE; |
| 208 | } | 208 | } |
| 209 | } | 209 | } |
| 210 | + | ||
| 211 | +.entity-selector { | ||
| 212 | + border: 1px solid #DDD; | ||
| 213 | + border-radius: 3px; | ||
| 214 | + overflow: hidden; | ||
| 215 | + font-size: 0.8em; | ||
| 216 | + input[type="text"] { | ||
| 217 | + width: 100%; | ||
| 218 | + display: block; | ||
| 219 | + border-radius: 0; | ||
| 220 | + border: 0; | ||
| 221 | + border-bottom: 1px solid #DDD; | ||
| 222 | + font-size: 16px; | ||
| 223 | + padding: $-s $-m; | ||
| 224 | + } | ||
| 225 | + .entity-list { | ||
| 226 | + overflow-y: scroll; | ||
| 227 | + height: 400px; | ||
| 228 | + background-color: #EEEEEE; | ||
| 229 | + } | ||
| 230 | + .loading { | ||
| 231 | + height: 400px; | ||
| 232 | + padding-top: $-l; | ||
| 233 | + } | ||
| 234 | + .entity-list > p { | ||
| 235 | + text-align: center; | ||
| 236 | + padding-top: $-l; | ||
| 237 | + font-size: 1.333em; | ||
| 238 | + } | ||
| 239 | + .entity-list > div { | ||
| 240 | + padding-left: $-m; | ||
| 241 | + padding-right: $-m; | ||
| 242 | + background-color: #FFF; | ||
| 243 | + transition: all ease-in-out 120ms; | ||
| 244 | + cursor: pointer; | ||
| 245 | + } | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +.entity-list-item.selected { | ||
| 249 | + h3, i, p ,a, span { | ||
| 250 | + color: #EEE; | ||
| 251 | + } | ||
| 252 | +} | ||
| 253 | + | ||
| 254 | + | ||
| 255 | + | ||
| 256 | + | ||
| 257 | + | ||
| 258 | + | ||
| 259 | + | ||
| 260 | + | ||
| 261 | + | ||
| 262 | + | ||
| 263 | + | ||
| 264 | + | ||
| 265 | + | ... | ... |
| ... | @@ -4,7 +4,7 @@ return [ | ... | @@ -4,7 +4,7 @@ return [ |
| 4 | 4 | ||
| 5 | /** | 5 | /** |
| 6 | * Activity text strings. | 6 | * Activity text strings. |
| 7 | - * Is used for all the text within activity logs. | 7 | + * Is used for all the text within activity logs & notifications. |
| 8 | */ | 8 | */ |
| 9 | 9 | ||
| 10 | // Pages | 10 | // Pages |
| ... | @@ -16,6 +16,7 @@ return [ | ... | @@ -16,6 +16,7 @@ return [ |
| 16 | 'page_delete_notification' => 'Page Successfully Deleted', | 16 | 'page_delete_notification' => 'Page Successfully Deleted', |
| 17 | 'page_restore' => 'restored page', | 17 | 'page_restore' => 'restored page', |
| 18 | 'page_restore_notification' => 'Page Successfully Restored', | 18 | 'page_restore_notification' => 'Page Successfully Restored', |
| 19 | + 'page_move' => 'moved page', | ||
| 19 | 20 | ||
| 20 | // Chapters | 21 | // Chapters |
| 21 | 'chapter_create' => 'created chapter', | 22 | 'chapter_create' => 'created chapter', |
| ... | @@ -24,6 +25,7 @@ return [ | ... | @@ -24,6 +25,7 @@ return [ |
| 24 | 'chapter_update_notification' => 'Chapter Successfully Updated', | 25 | 'chapter_update_notification' => 'Chapter Successfully Updated', |
| 25 | 'chapter_delete' => 'deleted chapter', | 26 | 'chapter_delete' => 'deleted chapter', |
| 26 | 'chapter_delete_notification' => 'Chapter Successfully Deleted', | 27 | 'chapter_delete_notification' => 'Chapter Successfully Deleted', |
| 28 | + 'chapter_move' => 'moved chapter', | ||
| 27 | 29 | ||
| 28 | // Books | 30 | // Books |
| 29 | 'book_create' => 'created book', | 31 | 'book_create' => 'created book', | ... | ... |
| 1 | -<div class="book"> | 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" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i>{{$book->name}}</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> | ... | ... |
| 1 | -<div class="chapter"> | 1 | +<div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}"> |
| 2 | <h3> | 2 | <h3> |
| 3 | + @if (isset($showPath) && $showPath) | ||
| 4 | + <a href="{{ $chapter->book->getUrl() }}" class="text-book"> | ||
| 5 | + <i class="zmdi zmdi-book"></i>{{ $chapter->book->name }} | ||
| 6 | + </a> | ||
| 7 | + <span class="text-muted"> » </span> | ||
| 8 | + @endif | ||
| 3 | <a href="{{ $chapter->getUrl() }}" class="text-chapter"> | 9 | <a href="{{ $chapter->getUrl() }}" class="text-chapter"> |
| 4 | <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }} | 10 | <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }} |
| 5 | </a> | 11 | </a> | ... | ... |
resources/views/chapters/move.blade.php
0 → 100644
| 1 | +@extends('base') | ||
| 2 | + | ||
| 3 | +@section('content') | ||
| 4 | + | ||
| 5 | + <div class="faded-small toolbar"> | ||
| 6 | + <div class="container"> | ||
| 7 | + <div class="row"> | ||
| 8 | + <div class="col-sm-12 faded"> | ||
| 9 | + <div class="breadcrumbs"> | ||
| 10 | + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a> | ||
| 11 | + <span class="sep">»</span> | ||
| 12 | + <a href="{{$chapter->getUrl()}}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->getShortName() }}</a> | ||
| 13 | + </div> | ||
| 14 | + </div> | ||
| 15 | + </div> | ||
| 16 | + </div> | ||
| 17 | + </div> | ||
| 18 | + | ||
| 19 | + <div class="container"> | ||
| 20 | + <h1>Move Chapter <small class="subheader">{{$chapter->name}}</small></h1> | ||
| 21 | + | ||
| 22 | + <form action="{{ $chapter->getUrl() }}/move" method="POST"> | ||
| 23 | + {!! csrf_field() !!} | ||
| 24 | + <input type="hidden" name="_method" value="PUT"> | ||
| 25 | + | ||
| 26 | + @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book']) | ||
| 27 | + | ||
| 28 | + <a href="{{ $chapter->getUrl() }}" class="button muted">Cancel</a> | ||
| 29 | + <button type="submit" class="button pos">Move Chapter</button> | ||
| 30 | + </form> | ||
| 31 | + </div> | ||
| 32 | + | ||
| 33 | +@stop |
| ... | @@ -2,15 +2,15 @@ | ... | @@ -2,15 +2,15 @@ |
| 2 | 2 | ||
| 3 | @section('content') | 3 | @section('content') |
| 4 | 4 | ||
| 5 | - <div class="faded-small toolbar" ng-non-bindable> | 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-4 faded"> | 8 | + <div class="col-sm-8 faded" ng-non-bindable> |
| 9 | <div class="breadcrumbs"> | 9 | <div class="breadcrumbs"> |
| 10 | <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a> | 10 | <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a> |
| 11 | </div> | 11 | </div> |
| 12 | </div> | 12 | </div> |
| 13 | - <div class="col-md-8 faded"> | 13 | + <div class="col-sm-4 faded"> |
| 14 | <div class="action-buttons"> | 14 | <div class="action-buttons"> |
| 15 | @if(userCan('page-create', $chapter)) | 15 | @if(userCan('page-create', $chapter)) |
| 16 | <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a> | 16 | <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a> |
| ... | @@ -18,11 +18,21 @@ | ... | @@ -18,11 +18,21 @@ |
| 18 | @if(userCan('chapter-update', $chapter)) | 18 | @if(userCan('chapter-update', $chapter)) |
| 19 | <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> | 19 | <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> |
| 20 | @endif | 20 | @endif |
| 21 | - @if(userCan('restrictions-manage', $chapter)) | 21 | + @if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter)) |
| 22 | - <a href="{{$chapter->getUrl()}}/permissions" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Permissions</a> | 22 | + <div dropdown class="dropdown-container"> |
| 23 | - @endif | 23 | + <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a> |
| 24 | - @if(userCan('chapter-delete', $chapter)) | 24 | + <ul> |
| 25 | - <a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> | 25 | + @if(userCan('chapter-update', $chapter)) |
| 26 | + <li><a href="{{$chapter->getUrl() . '/move'}}" class="text-primary"><i class="zmdi zmdi-folder"></i>Move</a></li> | ||
| 27 | + @endif | ||
| 28 | + @if(userCan('restrictions-manage', $chapter)) | ||
| 29 | + <li><a href="{{$chapter->getUrl()}}/permissions" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li> | ||
| 30 | + @endif | ||
| 31 | + @if(userCan('chapter-delete', $chapter)) | ||
| 32 | + <li><a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li> | ||
| 33 | + @endif | ||
| 34 | + </ul> | ||
| 35 | + </div> | ||
| 26 | @endif | 36 | @endif |
| 27 | </div> | 37 | </div> |
| 28 | </div> | 38 | </div> | ... | ... |
| ... | @@ -34,18 +34,30 @@ | ... | @@ -34,18 +34,30 @@ |
| 34 | @else | 34 | @else |
| 35 | <h3>Recent Books</h3> | 35 | <h3>Recent Books</h3> |
| 36 | @endif | 36 | @endif |
| 37 | - @include('partials/entity-list', ['entities' => $recents, 'style' => 'compact']) | 37 | + @include('partials/entity-list', [ |
| 38 | + 'entities' => $recents, | ||
| 39 | + 'style' => 'compact', | ||
| 40 | + 'emptyText' => $signedIn ? 'You have not viewed any pages' : 'No books have been created' | ||
| 41 | + ]) | ||
| 38 | </div> | 42 | </div> |
| 39 | 43 | ||
| 40 | <div class="col-sm-4"> | 44 | <div class="col-sm-4"> |
| 41 | <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3> | 45 | <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3> |
| 42 | <div id="recently-created-pages"> | 46 | <div id="recently-created-pages"> |
| 43 | - @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) | 47 | + @include('partials/entity-list', [ |
| 48 | + 'entities' => $recentlyCreatedPages, | ||
| 49 | + 'style' => 'compact', | ||
| 50 | + 'emptyText' => 'No pages have been recently created' | ||
| 51 | + ]) | ||
| 44 | </div> | 52 | </div> |
| 45 | 53 | ||
| 46 | <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3> | 54 | <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3> |
| 47 | <div id="recently-updated-pages"> | 55 | <div id="recently-updated-pages"> |
| 48 | - @include('partials/entity-list', ['entities' => $recentlyUpdatedPages, 'style' => 'compact']) | 56 | + @include('partials/entity-list', [ |
| 57 | + 'entities' => $recentlyUpdatedPages, | ||
| 58 | + 'style' => 'compact', | ||
| 59 | + 'emptyText' => 'No pages have been recently updated' | ||
| 60 | + ]) | ||
| 49 | </div> | 61 | </div> |
| 50 | </div> | 62 | </div> |
| 51 | 63 | ... | ... |
| ... | @@ -10,12 +10,12 @@ | ... | @@ -10,12 +10,12 @@ |
| 10 | <h4>Page Tags</h4> | 10 | <h4>Page Tags</h4> |
| 11 | <div class="padded tags"> | 11 | <div class="padded tags"> |
| 12 | <p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p> | 12 | <p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p> |
| 13 | - <table class="no-style" autosuggestions style="width: 100%;"> | 13 | + <table class="no-style" tag-autosuggestions style="width: 100%;"> |
| 14 | <tbody ui-sortable="sortOptions" ng-model="tags" > | 14 | <tbody ui-sortable="sortOptions" ng-model="tags" > |
| 15 | <tr ng-repeat="tag in tags track by $index"> | 15 | <tr ng-repeat="tag in tags track by $index"> |
| 16 | <td width="20" ><i class="handle zmdi zmdi-menu"></i></td> | 16 | <td width="20" ><i class="handle zmdi zmdi-menu"></i></td> |
| 17 | - <td><input autosuggest="/ajax/tags/suggest/names" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td> | 17 | + <td><input autosuggest="/ajax/tags/suggest/names" autosuggest-type="name" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td> |
| 18 | - <td><input autosuggest="/ajax/tags/suggest/values" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td> | 18 | + <td><input autosuggest="/ajax/tags/suggest/values" autosuggest-type="value" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td> |
| 19 | <td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td> | 19 | <td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td> |
| 20 | </tr> | 20 | </tr> |
| 21 | </tbody> | 21 | </tbody> | ... | ... |
| ... | @@ -61,7 +61,7 @@ | ... | @@ -61,7 +61,7 @@ |
| 61 | <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button> | 61 | <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button> |
| 62 | </div> | 62 | </div> |
| 63 | </div> | 63 | </div> |
| 64 | - <textarea markdown-input md-change="editorChange" md-model="editContent" name="markdown" rows="5" | 64 | + <textarea markdown-input md-change="editorChange" id="markdown-editor-input" md-model="editContent" name="markdown" rows="5" |
| 65 | @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea> | 65 | @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea> |
| 66 | </div> | 66 | </div> |
| 67 | 67 | ... | ... |
| 1 | -<div class="page {{$page->draft ? 'draft' : ''}}"> | 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"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a> |
| 4 | </h3> | 4 | </h3> |
| ... | @@ -11,11 +11,11 @@ | ... | @@ -11,11 +11,11 @@ |
| 11 | 11 | ||
| 12 | @if(isset($style) && $style === 'detailed') | 12 | @if(isset($style) && $style === 'detailed') |
| 13 | <div class="row meta text-muted text-small"> | 13 | <div class="row meta text-muted text-small"> |
| 14 | - <div class="col-md-4"> | 14 | + <div class="col-md-6"> |
| 15 | Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif <br> | 15 | Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif <br> |
| 16 | Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif | 16 | Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif |
| 17 | </div> | 17 | </div> |
| 18 | - <div class="col-md-8"> | 18 | + <div class="col-md-6"> |
| 19 | <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a> | 19 | <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a> |
| 20 | <br> | 20 | <br> |
| 21 | @if($page->chapter) | 21 | @if($page->chapter) | ... | ... |
resources/views/pages/move.blade.php
0 → 100644
| 1 | +@extends('base') | ||
| 2 | + | ||
| 3 | +@section('content') | ||
| 4 | + | ||
| 5 | + <div class="faded-small toolbar"> | ||
| 6 | + <div class="container"> | ||
| 7 | + <div class="row"> | ||
| 8 | + <div class="col-sm-12 faded"> | ||
| 9 | + <div class="breadcrumbs"> | ||
| 10 | + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $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-text"></i>{{ $page->getShortName() }}</a> | ||
| 20 | + </div> | ||
| 21 | + </div> | ||
| 22 | + </div> | ||
| 23 | + </div> | ||
| 24 | + </div> | ||
| 25 | + | ||
| 26 | + <div class="container"> | ||
| 27 | + <h1>Move Page <small class="subheader">{{$page->name}}</small></h1> | ||
| 28 | + | ||
| 29 | + <form action="{{ $page->getUrl() }}/move" method="POST"> | ||
| 30 | + {!! csrf_field() !!} | ||
| 31 | + <input type="hidden" name="_method" value="PUT"> | ||
| 32 | + | ||
| 33 | + @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter']) | ||
| 34 | + | ||
| 35 | + <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a> | ||
| 36 | + <button type="submit" class="button pos">Move Page</button> | ||
| 37 | + </form> | ||
| 38 | + </div> | ||
| 39 | + | ||
| 40 | +@stop |
| ... | @@ -28,15 +28,26 @@ | ... | @@ -28,15 +28,26 @@ |
| 28 | </ul> | 28 | </ul> |
| 29 | </span> | 29 | </span> |
| 30 | @if(userCan('page-update', $page)) | 30 | @if(userCan('page-update', $page)) |
| 31 | - <a href="{{$page->getUrl()}}/revisions" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a> | ||
| 32 | <a href="{{$page->getUrl()}}/edit" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a> | 31 | <a href="{{$page->getUrl()}}/edit" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a> |
| 33 | @endif | 32 | @endif |
| 34 | - @if(userCan('restrictions-manage', $page)) | 33 | + @if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page)) |
| 35 | - <a href="{{$page->getUrl()}}/permissions" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Permissions</a> | 34 | + <div dropdown class="dropdown-container"> |
| 36 | - @endif | 35 | + <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a> |
| 37 | - @if(userCan('page-delete', $page)) | 36 | + <ul> |
| 38 | - <a href="{{$page->getUrl()}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> | 37 | + @if(userCan('page-update', $page)) |
| 38 | + <li><a href="{{$page->getUrl()}}/move" class="text-primary" ><i class="zmdi zmdi-folder"></i>Move</a></li> | ||
| 39 | + <li><a href="{{$page->getUrl()}}/revisions" class="text-primary"><i class="zmdi zmdi-replay"></i>Revisions</a></li> | ||
| 40 | + @endif | ||
| 41 | + @if(userCan('restrictions-manage', $page)) | ||
| 42 | + <li><a href="{{$page->getUrl()}}/permissions" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li> | ||
| 43 | + @endif | ||
| 44 | + @if(userCan('page-delete', $page)) | ||
| 45 | + <li><a href="{{$page->getUrl()}}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li> | ||
| 46 | + @endif | ||
| 47 | + </ul> | ||
| 48 | + </div> | ||
| 39 | @endif | 49 | @endif |
| 50 | + | ||
| 40 | </div> | 51 | </div> |
| 41 | </div> | 52 | </div> |
| 42 | </div> | 53 | </div> | ... | ... |
| 1 | 1 | ||
| 2 | -{{--Requires an entity to be passed with the name $entity--}} | ||
| 3 | - | ||
| 4 | @if(count($activity) > 0) | 2 | @if(count($activity) > 0) |
| 5 | <div class="activity-list"> | 3 | <div class="activity-list"> |
| 6 | @foreach($activity as $activityItem) | 4 | @foreach($activity as $activityItem) |
| ... | @@ -10,5 +8,5 @@ | ... | @@ -10,5 +8,5 @@ |
| 10 | @endforeach | 8 | @endforeach |
| 11 | </div> | 9 | </div> |
| 12 | @else | 10 | @else |
| 13 | - <p class="text-muted">New activity will show up here.</p> | 11 | + <p class="text-muted">No activity to show</p> |
| 14 | @endif | 12 | @endif |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | -@if(Setting::get('app-color')) | ||
| 2 | - <style> | ||
| 3 | - header, #back-to-top, .primary-background { | ||
| 4 | - background-color: {{ Setting::get('app-color') }}; | ||
| 5 | - } | ||
| 6 | - .faded-small, .primary-background-light { | ||
| 7 | - background-color: {{ Setting::get('app-color-light') }}; | ||
| 8 | - } | ||
| 9 | - .button-base, .button, input[type="button"], input[type="submit"] { | ||
| 10 | - background-color: {{ Setting::get('app-color') }}; | ||
| 11 | - } | ||
| 12 | - .button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus { | ||
| 13 | - background-color: {{ Setting::get('app-color') }}; | ||
| 14 | - } | ||
| 15 | - .nav-tabs a.selected, .nav-tabs .tab-item.selected { | ||
| 16 | - border-bottom-color: {{ Setting::get('app-color') }}; | ||
| 17 | - } | ||
| 18 | - p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus { | ||
| 19 | - color: {{ Setting::get('app-color') }}; | ||
| 20 | - } | ||
| 21 | - </style> | ||
| 22 | -@endif | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | +<style> | ||
| 2 | + header, #back-to-top, .primary-background { | ||
| 3 | + background-color: {{ Setting::get('app-color') }} !important; | ||
| 4 | + } | ||
| 5 | + .faded-small, .primary-background-light { | ||
| 6 | + background-color: {{ Setting::get('app-color-light') }}; | ||
| 7 | + } | ||
| 8 | + .button-base, .button, input[type="button"], input[type="submit"] { | ||
| 9 | + background-color: {{ Setting::get('app-color') }}; | ||
| 10 | + } | ||
| 11 | + .button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus { | ||
| 12 | + background-color: {{ Setting::get('app-color') }}; | ||
| 13 | + } | ||
| 14 | + .nav-tabs a.selected, .nav-tabs .tab-item.selected { | ||
| 15 | + border-bottom-color: {{ Setting::get('app-color') }}; | ||
| 16 | + } | ||
| 17 | + p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus { | ||
| 18 | + color: {{ Setting::get('app-color') }}; | ||
| 19 | + } | ||
| 20 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -16,8 +16,8 @@ | ... | @@ -16,8 +16,8 @@ |
| 16 | 16 | ||
| 17 | @endforeach | 17 | @endforeach |
| 18 | @else | 18 | @else |
| 19 | - <p class="text-muted"> | 19 | + <p class="text-muted empty-text"> |
| 20 | - No items available | 20 | + {{ $emptyText or 'No items available' }} |
| 21 | </p> | 21 | </p> |
| 22 | @endif | 22 | @endif |
| 23 | </div> | 23 | </div> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | +<div class="form-group"> | ||
| 2 | + <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}"> | ||
| 3 | + <input type="hidden" entity-selector-input name="{{$name}}" value=""> | ||
| 4 | + <input type="text" placeholder="Search" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()"> | ||
| 5 | + <div class="text-center loading" ng-show="loading">@include('partials/loading-icon')</div> | ||
| 6 | + <div ng-show="!loading" ng-bind-html="entityResults"></div> | ||
| 7 | + </div> | ||
| 8 | +</div> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | 1 | ||
| 2 | -<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif> | 2 | +<div class="notification anim pos" @if(!session()->has('success')) style="display:none;" @endif> |
| 3 | - <i class="zmdi zmdi-check-circle"></i> <span>{!! nl2br(htmlentities(Session::get('success'))) !!}</span> | 3 | + <i class="zmdi zmdi-check-circle"></i> <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span> |
| 4 | </div> | 4 | </div> |
| 5 | 5 | ||
| 6 | -<div class="notification anim warning stopped" @if(!Session::has('warning')) style="display:none;" @endif> | 6 | +<div class="notification anim warning stopped" @if(!session()->has('warning')) style="display:none;" @endif> |
| 7 | - <i class="zmdi zmdi-info"></i> <span>{!! nl2br(htmlentities(Session::get('warning'))) !!}</span> | 7 | + <i class="zmdi zmdi-info"></i> <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span> |
| 8 | </div> | 8 | </div> |
| 9 | 9 | ||
| 10 | -<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif> | 10 | +<div class="notification anim neg stopped" @if(!session()->has('error')) style="display:none;" @endif> |
| 11 | - <i class="zmdi zmdi-alert-circle"></i> <span>{!! nl2br(htmlentities(Session::get('error'))) !!}</span> | 11 | + <i class="zmdi zmdi-alert-circle"></i> <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span> |
| 12 | </div> | 12 | </div> | ... | ... |
| 1 | +<div class="entity-list @if(isset($style)){{ $style }}@endif" ng-non-bindable> | ||
| 2 | + @if(count($entities) > 0) | ||
| 3 | + @foreach($entities as $index => $entity) | ||
| 4 | + @if($entity->isA('page')) | ||
| 5 | + @include('pages/list-item', ['page' => $entity]) | ||
| 6 | + @elseif($entity->isA('book')) | ||
| 7 | + @include('books/list-item', ['book' => $entity]) | ||
| 8 | + @elseif($entity->isA('chapter')) | ||
| 9 | + @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true, 'showPath' => true]) | ||
| 10 | + @endif | ||
| 11 | + | ||
| 12 | + @if($index !== count($entities) - 1) | ||
| 13 | + <hr> | ||
| 14 | + @endif | ||
| 15 | + | ||
| 16 | + @endforeach | ||
| 17 | + @else | ||
| 18 | + <p class="text-muted"> | ||
| 19 | + No items available | ||
| 20 | + </p> | ||
| 21 | + @endif | ||
| 22 | +</div> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -82,4 +82,14 @@ class EntitySearchTest extends TestCase | ... | @@ -82,4 +82,14 @@ class EntitySearchTest extends TestCase |
| 82 | $this->asAdmin()->visit('/search/books?term=' . $book->name) | 82 | $this->asAdmin()->visit('/search/books?term=' . $book->name) |
| 83 | ->see('Book Search Results')->see('.entity-list', $book->name); | 83 | ->see('Book Search Results')->see('.entity-list', $book->name); |
| 84 | } | 84 | } |
| 85 | + | ||
| 86 | + public function test_ajax_entity_search() | ||
| 87 | + { | ||
| 88 | + $page = \BookStack\Page::all()->last(); | ||
| 89 | + $notVisitedPage = \BookStack\Page::first(); | ||
| 90 | + $this->visit($page->getUrl()); | ||
| 91 | + $this->asAdmin()->visit('/ajax/search/entities?term=' . $page->name)->see('.entity-list', $page->name); | ||
| 92 | + $this->asAdmin()->visit('/ajax/search/entities?types=book&term=' . $page->name)->dontSee('.entity-list', $page->name); | ||
| 93 | + $this->asAdmin()->visit('/ajax/search/entities')->see('.entity-list', $page->name)->dontSee($notVisitedPage->name); | ||
| 94 | + } | ||
| 85 | } | 95 | } | ... | ... |
| ... | @@ -22,4 +22,47 @@ class SortTest extends TestCase | ... | @@ -22,4 +22,47 @@ class SortTest extends TestCase |
| 22 | ->dontSee($draft->name); | 22 | ->dontSee($draft->name); |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | + public function test_page_move() | ||
| 26 | + { | ||
| 27 | + $page = \BookStack\Page::first(); | ||
| 28 | + $currentBook = $page->book; | ||
| 29 | + $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first(); | ||
| 30 | + $this->asAdmin()->visit($page->getUrl() . '/move') | ||
| 31 | + ->see('Move Page')->see($page->name) | ||
| 32 | + ->type('book:' . $newBook->id, 'entity_selection')->press('Move Page'); | ||
| 33 | + | ||
| 34 | + $page = \BookStack\Page::find($page->id); | ||
| 35 | + $this->seePageIs($page->getUrl()); | ||
| 36 | + $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); | ||
| 37 | + | ||
| 38 | + $this->visit($newBook->getUrl()) | ||
| 39 | + ->seeInNthElement('.activity-list-item', 0, 'moved page') | ||
| 40 | + ->seeInNthElement('.activity-list-item', 0, $page->name); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + public function test_chapter_move() | ||
| 44 | + { | ||
| 45 | + $chapter = \BookStack\Chapter::first(); | ||
| 46 | + $currentBook = $chapter->book; | ||
| 47 | + $pageToCheck = $chapter->pages->first(); | ||
| 48 | + $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first(); | ||
| 49 | + | ||
| 50 | + $this->asAdmin()->visit($chapter->getUrl() . '/move') | ||
| 51 | + ->see('Move Chapter')->see($chapter->name) | ||
| 52 | + ->type('book:' . $newBook->id, 'entity_selection')->press('Move Chapter'); | ||
| 53 | + | ||
| 54 | + $chapter = \BookStack\Chapter::find($chapter->id); | ||
| 55 | + $this->seePageIs($chapter->getUrl()); | ||
| 56 | + $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book'); | ||
| 57 | + | ||
| 58 | + $this->visit($newBook->getUrl()) | ||
| 59 | + ->seeInNthElement('.activity-list-item', 0, 'moved chapter') | ||
| 60 | + ->seeInNthElement('.activity-list-item', 0, $chapter->name); | ||
| 61 | + | ||
| 62 | + $pageToCheck = \BookStack\Page::find($pageToCheck->id); | ||
| 63 | + $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book'); | ||
| 64 | + $this->visit($pageToCheck->getUrl()) | ||
| 65 | + ->see($newBook->name); | ||
| 66 | + } | ||
| 67 | + | ||
| 25 | } | 68 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
tests/ImageTest.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +class ImageTest extends TestCase | ||
| 4 | +{ | ||
| 5 | + | ||
| 6 | + /** | ||
| 7 | + * Get a test image that can be uploaded | ||
| 8 | + * @param $fileName | ||
| 9 | + * @return \Illuminate\Http\UploadedFile | ||
| 10 | + */ | ||
| 11 | + protected function getTestImage($fileName) | ||
| 12 | + { | ||
| 13 | + return new \Illuminate\Http\UploadedFile(base_path('tests/test-image.jpg'), $fileName, 'image/jpeg', 5238); | ||
| 14 | + } | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * Get the path for a test image. | ||
| 18 | + * @param $type | ||
| 19 | + * @param $fileName | ||
| 20 | + * @return string | ||
| 21 | + */ | ||
| 22 | + protected function getTestImagePath($type, $fileName) | ||
| 23 | + { | ||
| 24 | + return '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/' . $fileName; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * Uploads an image with the given name. | ||
| 29 | + * @param $name | ||
| 30 | + * @param int $uploadedTo | ||
| 31 | + * @return string | ||
| 32 | + */ | ||
| 33 | + protected function uploadImage($name, $uploadedTo = 0) | ||
| 34 | + { | ||
| 35 | + $file = $this->getTestImage($name); | ||
| 36 | + $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); | ||
| 37 | + return $this->getTestImagePath('gallery', $name); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * Delete an uploaded image. | ||
| 42 | + * @param $relPath | ||
| 43 | + */ | ||
| 44 | + protected function deleteImage($relPath) | ||
| 45 | + { | ||
| 46 | + unlink(public_path($relPath)); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + | ||
| 50 | + public function test_image_upload() | ||
| 51 | + { | ||
| 52 | + $page = \BookStack\Page::first(); | ||
| 53 | + $this->asAdmin(); | ||
| 54 | + $admin = $this->getAdmin(); | ||
| 55 | + $imageName = 'first-image.jpg'; | ||
| 56 | + | ||
| 57 | + $relPath = $this->uploadImage($imageName, $page->id); | ||
| 58 | + $this->assertResponseOk(); | ||
| 59 | + | ||
| 60 | + $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists'); | ||
| 61 | + | ||
| 62 | + $this->seeInDatabase('images', [ | ||
| 63 | + 'url' => $relPath, | ||
| 64 | + 'type' => 'gallery', | ||
| 65 | + 'uploaded_to' => $page->id, | ||
| 66 | + 'path' => $relPath, | ||
| 67 | + 'created_by' => $admin->id, | ||
| 68 | + 'updated_by' => $admin->id, | ||
| 69 | + 'name' => $imageName | ||
| 70 | + ]); | ||
| 71 | + | ||
| 72 | + $this->deleteImage($relPath); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + public function test_image_delete() | ||
| 76 | + { | ||
| 77 | + $page = \BookStack\Page::first(); | ||
| 78 | + $this->asAdmin(); | ||
| 79 | + $imageName = 'first-image.jpg'; | ||
| 80 | + | ||
| 81 | + $relPath = $this->uploadImage($imageName, $page->id); | ||
| 82 | + $image = \BookStack\Image::first(); | ||
| 83 | + | ||
| 84 | + $this->call('DELETE', '/images/' . $image->id); | ||
| 85 | + $this->assertResponseOk(); | ||
| 86 | + | ||
| 87 | + $this->dontSeeInDatabase('images', [ | ||
| 88 | + 'url' => $relPath, | ||
| 89 | + 'type' => 'gallery' | ||
| 90 | + ]); | ||
| 91 | + | ||
| 92 | + $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has been deleted'); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -39,11 +39,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase | ... | @@ -39,11 +39,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase |
| 39 | */ | 39 | */ |
| 40 | public function asAdmin() | 40 | public function asAdmin() |
| 41 | { | 41 | { |
| 42 | + return $this->actingAs($this->getAdmin()); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * Get the current admin user. | ||
| 47 | + * @return mixed | ||
| 48 | + */ | ||
| 49 | + public function getAdmin() { | ||
| 42 | if($this->admin === null) { | 50 | if($this->admin === null) { |
| 43 | $adminRole = \BookStack\Role::getRole('admin'); | 51 | $adminRole = \BookStack\Role::getRole('admin'); |
| 44 | $this->admin = $adminRole->users->first(); | 52 | $this->admin = $adminRole->users->first(); |
| 45 | } | 53 | } |
| 46 | - return $this->actingAs($this->admin); | 54 | + return $this->admin; |
| 47 | } | 55 | } |
| 48 | 56 | ||
| 49 | /** | 57 | /** | ... | ... |
tests/test-image.jpg
0 → 100644
5.12 KB
-
Please register or sign in to post a comment