Dan Brown

Merged branch add_page_move into master

References #86
...@@ -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,59 @@ class PageController extends Controller ...@@ -451,6 +451,59 @@ 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 + public function move($bookSlug, $pageSlug, Request $request)
472 + {
473 + $book = $this->bookRepo->getBySlug($bookSlug);
474 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
475 + $this->checkOwnablePermission('page-update', $page);
476 +
477 + $entitySelection = $request->get('entity_selection', null);
478 + if ($entitySelection === null || $entitySelection === '') {
479 + return redirect($page->getUrl());
480 + }
481 +
482 + $stringExploded = explode(':', $entitySelection);
483 + $entityType = $stringExploded[0];
484 + $entityId = intval($stringExploded[1]);
485 +
486 + $parent = false;
487 +
488 + if ($entityType == 'chapter') {
489 + $parent = $this->chapterRepo->getById($entityId);
490 + } else if ($entityType == 'book') {
491 + $parent = $this->bookRepo->getById($entityId);
492 + }
493 +
494 + if ($parent === false || $parent === null) {
495 + session()->flash('The selected Book or Chapter was not found');
496 + return redirect()->back();
497 + }
498 +
499 + $this->pageRepo->changePageParent($page, $parent);
500 + Activity::add($page, 'page_move', $page->book->id);
501 + session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
502 +
503 + return redirect($page->getUrl());
504 + }
505 +
506 + /**
454 * Set the permissions for this page. 507 * Set the permissions for this page.
455 * @param $bookSlug 508 * @param $bookSlug
456 * @param $pageSlug 509 * @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('partials/entity-list', ['entities' => $entities]);
167 + }
168 +
137 } 169 }
170 +
171 +
......
...@@ -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');
...@@ -93,6 +95,8 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -93,6 +95,8 @@ Route::group(['middleware' => 'auth'], function () {
93 Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity'); 95 Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
94 }); 96 });
95 97
98 + Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
99 +
96 // Links 100 // Links
97 Route::get('/link/{id}', 'PageController@redirectFromLink'); 101 Route::get('/link/{id}', 'PageController@redirectFromLink');
98 102
......
...@@ -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
......
...@@ -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 }
......
...@@ -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,6 +169,10 @@ module.exports = function (ngApp, events) { ...@@ -166,6 +169,10 @@ module.exports = function (ngApp, events) {
166 }; 169 };
167 }]); 170 }]);
168 171
172 + /**
173 + * TinyMCE
174 + * An angular wrapper around the tinyMCE editor.
175 + */
169 ngApp.directive('tinymce', ['$timeout', function ($timeout) { 176 ngApp.directive('tinymce', ['$timeout', function ($timeout) {
170 return { 177 return {
171 restrict: 'A', 178 restrict: 'A',
...@@ -231,6 +238,10 @@ module.exports = function (ngApp, events) { ...@@ -231,6 +238,10 @@ module.exports = function (ngApp, events) {
231 } 238 }
232 }]); 239 }]);
233 240
241 + /**
242 + * Markdown input
243 + * Handles the logic for just the editor input field.
244 + */
234 ngApp.directive('markdownInput', ['$timeout', function ($timeout) { 245 ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
235 return { 246 return {
236 restrict: 'A', 247 restrict: 'A',
...@@ -263,6 +274,10 @@ module.exports = function (ngApp, events) { ...@@ -263,6 +274,10 @@ module.exports = function (ngApp, events) {
263 } 274 }
264 }]); 275 }]);
265 276
277 + /**
278 + * Markdown Editor
279 + * Handles all functionality of the markdown editor.
280 + */
266 ngApp.directive('markdownEditor', ['$timeout', function ($timeout) { 281 ngApp.directive('markdownEditor', ['$timeout', function ($timeout) {
267 return { 282 return {
268 restrict: 'A', 283 restrict: 'A',
...@@ -342,6 +357,11 @@ module.exports = function (ngApp, events) { ...@@ -342,6 +357,11 @@ module.exports = function (ngApp, events) {
342 } 357 }
343 }]); 358 }]);
344 359
360 + /**
361 + * Page Editor Toolbox
362 + * Controls all functionality for the sliding toolbox
363 + * on the page edit view.
364 + */
345 ngApp.directive('toolbox', [function () { 365 ngApp.directive('toolbox', [function () {
346 return { 366 return {
347 restrict: 'A', 367 restrict: 'A',
...@@ -378,6 +398,11 @@ module.exports = function (ngApp, events) { ...@@ -378,6 +398,11 @@ module.exports = function (ngApp, events) {
378 } 398 }
379 }]); 399 }]);
380 400
401 + /**
402 + * Tag Autosuggestions
403 + * Listens to child inputs and provides autosuggestions depending on field type
404 + * and input. Suggestions provided by server.
405 + */
381 ngApp.directive('tagAutosuggestions', ['$http', function ($http) { 406 ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
382 return { 407 return {
383 restrict: 'A', 408 restrict: 'A',
...@@ -557,6 +582,67 @@ module.exports = function (ngApp, events) { ...@@ -557,6 +582,67 @@ module.exports = function (ngApp, events) {
557 } 582 }
558 } 583 }
559 }]); 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 + }]);
560 }; 646 };
561 647
562 648
......
...@@ -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 {
......
...@@ -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
......
...@@ -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 {
250 + color: #EEE;
251 + }
252 +}
253 +
254 +
255 +
256 +
257 +
258 +
259 +
260 +
261 +
262 +
263 +
264 +
265 +
......
...@@ -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',
......
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 <a href="{{ $chapter->getUrl() }}" class="text-chapter"> 3 <a href="{{ $chapter->getUrl() }}" class="text-chapter">
4 <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }} 4 <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }}
......
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 + <div class="form-group">
34 + <div entity-selector class="entity-selector large" entity-types="book,chapter">
35 + <input type="hidden" entity-selector-input name="entity_selection" value="">
36 + <input type="text" placeholder="Search" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
37 + <div class="text-center loading" ng-show="loading">@include('partials/loading-icon')</div>
38 + <div ng-show="!loading" ng-bind-html="entityResults"></div>
39 + </div>
40 + </div>
41 +
42 + <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a>
43 + <button type="submit" class="button pos">Move Page</button>
44 + </form>
45 + </div>
46 +
47 +@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 @if(Setting::get('app-color')) 1 @if(Setting::get('app-color'))
2 <style> 2 <style>
3 header, #back-to-top, .primary-background { 3 header, #back-to-top, .primary-background {
4 - background-color: {{ Setting::get('app-color') }}; 4 + background-color: {{ Setting::get('app-color') }} !important;
5 } 5 }
6 .faded-small, .primary-background-light { 6 .faded-small, .primary-background-light {
7 background-color: {{ Setting::get('app-color-light') }}; 7 background-color: {{ Setting::get('app-color-light') }};
......
...@@ -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,22 @@ class SortTest extends TestCase ...@@ -22,4 +22,22 @@ 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 +
25 } 43 }
...\ No newline at end of file ...\ No newline at end of file
......