Dan Brown

Added chapter move actions. Closes #86

...@@ -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',
......
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-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
......