Dan Brown

Merge branch 'master' into release

Showing 57 changed files with 855 additions and 131 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
......
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 +[![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest)
4 +[![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
3 [![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](https://travis-ci.org/ssddanbrown/BookStack) 5 [![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](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.
......
...@@ -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 }
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
48 max-width: 100%; 48 max-width: 100%;
49 height:auto; 49 height:auto;
50 } 50 }
51 - h1, h2, h3, h4, h5, h6 { 51 + h1, h2, h3, h4, h5, h6, pre {
52 clear: left; 52 clear: left;
53 } 53 }
54 hr { 54 hr {
......
...@@ -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">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</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>
......
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">&raquo;</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)
......
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">&raquo;</span>
13 + <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
14 + <i class="zmdi zmdi-collection-bookmark"></i>
15 + {{$page->chapter->getShortName()}}
16 + </a>
17 + @endif
18 + <span class="sep">&raquo;</span>
19 + <a href="{{$page->getUrl()}}" class="text-page text-button"><i class="zmdi zmdi-file-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
......
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 /**
......