Merged branch add_page_move into master
References #86
Showing
18 changed files
with
378 additions
and
34 deletions
| ... | @@ -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) | ... | ... |
resources/views/pages/move.blade.php
0 → 100644
| 1 | +@extends('base') | ||
| 2 | + | ||
| 3 | +@section('content') | ||
| 4 | + | ||
| 5 | + <div class="faded-small toolbar"> | ||
| 6 | + <div class="container"> | ||
| 7 | + <div class="row"> | ||
| 8 | + <div class="col-sm-12 faded"> | ||
| 9 | + <div class="breadcrumbs"> | ||
| 10 | + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a> | ||
| 11 | + @if($page->hasChapter()) | ||
| 12 | + <span class="sep">»</span> | ||
| 13 | + <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button"> | ||
| 14 | + <i class="zmdi zmdi-collection-bookmark"></i> | ||
| 15 | + {{$page->chapter->getShortName()}} | ||
| 16 | + </a> | ||
| 17 | + @endif | ||
| 18 | + <span class="sep">»</span> | ||
| 19 | + <a href="{{$page->getUrl()}}" class="text-page text-button"><i class="zmdi zmdi-file-text"></i>{{ $page->getShortName() }}</a> | ||
| 20 | + </div> | ||
| 21 | + </div> | ||
| 22 | + </div> | ||
| 23 | + </div> | ||
| 24 | + </div> | ||
| 25 | + | ||
| 26 | + <div class="container"> | ||
| 27 | + <h1>Move Page <small class="subheader">{{$page->name}}</small></h1> | ||
| 28 | + | ||
| 29 | + <form action="{{ $page->getUrl() }}/move" method="POST"> | ||
| 30 | + {!! csrf_field() !!} | ||
| 31 | + <input type="hidden" name="_method" value="PUT"> | ||
| 32 | + | ||
| 33 | + <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 | ... | ... |
-
Please register or sign in to post a comment