Showing
10 changed files
with
167 additions
and
17 deletions
| ... | @@ -155,6 +155,55 @@ class ChapterController extends Controller | ... | @@ -155,6 +155,55 @@ 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 | + public function move($bookSlug, $chapterSlug, Request $request) { | ||
| 175 | + $book = $this->bookRepo->getBySlug($bookSlug); | ||
| 176 | + $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); | ||
| 177 | + $this->checkOwnablePermission('chapter-update', $chapter); | ||
| 178 | + | ||
| 179 | + $entitySelection = $request->get('entity_selection', null); | ||
| 180 | + if ($entitySelection === null || $entitySelection === '') { | ||
| 181 | + return redirect($chapter->getUrl()); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + $stringExploded = explode(':', $entitySelection); | ||
| 185 | + $entityType = $stringExploded[0]; | ||
| 186 | + $entityId = intval($stringExploded[1]); | ||
| 187 | + | ||
| 188 | + $parent = false; | ||
| 189 | + | ||
| 190 | + if ($entityType == 'book') { | ||
| 191 | + $parent = $this->bookRepo->getById($entityId); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + if ($parent === false || $parent === null) { | ||
| 195 | + session()->flash('The selected Book was not found'); | ||
| 196 | + return redirect()->back(); | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + $this->chapterRepo->changeBook($parent->id, $chapter); | ||
| 200 | + Activity::add($chapter, 'chapter_move', $chapter->book->id); | ||
| 201 | + session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name)); | ||
| 202 | + | ||
| 203 | + return redirect($chapter->getUrl()); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + /** | ||
| 158 | * Show the Restrictions view. | 207 | * Show the Restrictions view. |
| 159 | * @param $bookSlug | 208 | * @param $bookSlug |
| 160 | * @param $chapterSlug | 209 | * @param $chapterSlug | ... | ... |
| ... | @@ -468,6 +468,14 @@ class PageController extends Controller | ... | @@ -468,6 +468,14 @@ class PageController extends Controller |
| 468 | ]); | 468 | ]); |
| 469 | } | 469 | } |
| 470 | 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 | + */ | ||
| 471 | public function move($bookSlug, $pageSlug, Request $request) | 479 | public function move($bookSlug, $pageSlug, Request $request) |
| 472 | { | 480 | { |
| 473 | $book = $this->bookRepo->getBySlug($bookSlug); | 481 | $book = $this->bookRepo->getBySlug($bookSlug); | ... | ... |
| ... | @@ -55,6 +55,8 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -55,6 +55,8 @@ Route::group(['middleware' => 'auth'], function () { |
| 55 | Route::post('/{bookSlug}/chapter/create', 'ChapterController@store'); | 55 | Route::post('/{bookSlug}/chapter/create', 'ChapterController@store'); |
| 56 | Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); | 56 | Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); |
| 57 | 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'); | ||
| 58 | Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit'); | 60 | Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit'); |
| 59 | Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict'); | 61 | Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict'); |
| 60 | Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict'); | 62 | Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict'); | ... | ... |
| ... | @@ -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 | ... | ... |
| ... | @@ -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 |
| ... | @@ -25,6 +25,7 @@ return [ | ... | @@ -25,6 +25,7 @@ return [ |
| 25 | 'chapter_update_notification' => 'Chapter Successfully Updated', | 25 | 'chapter_update_notification' => 'Chapter Successfully Updated', |
| 26 | 'chapter_delete' => 'deleted chapter', | 26 | 'chapter_delete' => 'deleted chapter', |
| 27 | 'chapter_delete_notification' => 'Chapter Successfully Deleted', | 27 | 'chapter_delete_notification' => 'Chapter Successfully Deleted', |
| 28 | + 'chapter_move' => 'moved chapter', | ||
| 28 | 29 | ||
| 29 | // Books | 30 | // Books |
| 30 | 'book_create' => 'created book', | 31 | 'book_create' => 'created book', | ... | ... |
resources/views/chapters/move.blade.php
0 → 100644
| 1 | +@extends('base') | ||
| 2 | + | ||
| 3 | +@section('content') | ||
| 4 | + | ||
| 5 | + <div class="faded-small toolbar"> | ||
| 6 | + <div class="container"> | ||
| 7 | + <div class="row"> | ||
| 8 | + <div class="col-sm-12 faded"> | ||
| 9 | + <div class="breadcrumbs"> | ||
| 10 | + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a> | ||
| 11 | + <span class="sep">»</span> | ||
| 12 | + <a href="{{$chapter->getUrl()}}" class="text-page text-button"><i class="zmdi zmdi-file-text"></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> | ... | ... |
| ... | @@ -30,14 +30,7 @@ | ... | @@ -30,14 +30,7 @@ |
| 30 | {!! csrf_field() !!} | 30 | {!! csrf_field() !!} |
| 31 | <input type="hidden" name="_method" value="PUT"> | 31 | <input type="hidden" name="_method" value="PUT"> |
| 32 | 32 | ||
| 33 | - <div class="form-group"> | 33 | + @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter']) |
| 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 | 34 | ||
| 42 | <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a> | 35 | <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a> |
| 43 | <button type="submit" class="button pos">Move Page</button> | 36 | <button type="submit" class="button pos">Move Page</button> | ... | ... |
| 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 |
| ... | @@ -40,4 +40,29 @@ class SortTest extends TestCase | ... | @@ -40,4 +40,29 @@ class SortTest extends TestCase |
| 40 | ->seeInNthElement('.activity-list-item', 0, $page->name); | 40 | ->seeInNthElement('.activity-list-item', 0, $page->name); |
| 41 | } | 41 | } |
| 42 | 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 | + | ||
| 43 | } | 68 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
-
Please register or sign in to post a comment