diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php index 8b5213a8b..997cc041a 100644 --- a/app/Actions/ActivityType.php +++ b/app/Actions/ActivityType.php @@ -16,6 +16,7 @@ class ActivityType const CHAPTER_MOVE = 'chapter_move'; const BOOK_CREATE = 'book_create'; + const BOOK_CREATE_FROM_CHAPTER = 'book_create_from_chapter'; const BOOK_UPDATE = 'book_update'; const BOOK_DELETE = 'book_delete'; const BOOK_SORT = 'book_sort'; diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 0c62a13fc..b5944fd46 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -91,7 +91,7 @@ class BookRepo { $book = new Book(); $this->baseRepo->create($book, $input); - $this->baseRepo->updateCoverImage($book, $input['image']); + $this->baseRepo->updateCoverImage($book, $input['image'] ?? null); Activity::add(ActivityType::BOOK_CREATE, $book); return $book; @@ -104,7 +104,7 @@ class BookRepo { $this->baseRepo->update($book, $input); - if (isset($input['image'])) { + if (array_key_exists('image', $input)) { $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null); } diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php index c95d5fa53..7304962b3 100644 --- a/app/Entities/Tools/HierarchyTransformer.php +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -2,12 +2,14 @@ namespace BookStack\Entities\Tools; +use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookshelfRepo; +use BookStack\Facades\Activity; class HierarchyTransformer { @@ -16,10 +18,20 @@ class HierarchyTransformer protected Cloner $cloner; protected TrashCan $trashCan; + public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan) + { + $this->bookRepo = $bookRepo; + $this->shelfRepo = $shelfRepo; + $this->cloner = $cloner; + $this->trashCan = $trashCan; + } + + /** + * Transform a chapter into a book. + * Does not check permissions, check before calling. + */ public function transformChapterToBook(Chapter $chapter): Book { - // TODO - Check permissions before call - // Permissions: edit-chapter, delete-chapter, create-book $inputData = $this->cloner->entityToInputData($chapter); $book = $this->bookRepo->create($inputData); $this->cloner->copyEntityPermissions($chapter, $book); @@ -32,7 +44,7 @@ class HierarchyTransformer $this->trashCan->destroyEntity($chapter); - // TODO - Log activity for change + Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER); return $book; } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index 83b9bb692..d1fe5249a 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; +use BookStack\Entities\Tools\HierarchyTransformer; use BookStack\Entities\Tools\NextPreviousContentLocator; use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Exceptions\MoveOperationException; @@ -272,4 +273,20 @@ class ChapterController extends Controller return redirect($chapter->getUrl()); } + + + /** + * Convert the chapter to a book. + */ + public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug) + { + $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $this->checkOwnablePermission('chapter-update', $chapter); + $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkPermission('book-create-all'); + + $book = $transformer->transformChapterToBook($chapter); + + return redirect($book->getUrl()); + } } diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 77c39b50c..0c3d2e704 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -28,6 +28,8 @@ return [ // Books 'book_create' => 'created book', 'book_create_notification' => 'Book successfully created', + 'book_create_from_chapter' => 'converted chapter to book', + 'book_create_from_chapter_notification' => 'Chapter successfully converted to a book', 'book_update' => 'updated book', 'book_update_notification' => 'Book successfully updated', 'book_delete' => 'deleted book', diff --git a/resources/views/chapters/edit.blade.php b/resources/views/chapters/edit.blade.php index 65c48c18d..f6fd3cc6b 100644 --- a/resources/views/chapters/edit.blade.php +++ b/resources/views/chapters/edit.blade.php @@ -15,7 +15,7 @@ ]]) </div> - <main class="content-wrap card"> + <main class="content-wrap card auto-height"> <h1 class="list-heading">{{ trans('entities.chapters_edit') }}</h1> <form action="{{ $chapter->getUrl() }}" method="POST"> <input type="hidden" name="_method" value="PUT"> @@ -23,6 +23,36 @@ </form> </main> +{{-- TODO - Permissions--}} + <div class="content-wrap card auto-height"> + <h2 class="list-heading">Convert to Book</h2> + <div class="grid half left-focus no-row-gap"> + <p> + You can convert this chapter to a new book with the same contents. + Any permissions set on this chapter will be copied to the new book but any inherited permissions, + from the parent book, will not be copied which could lead to a change of access control. + </p> + <div class="text-m-right"> + <div component="dropdown" class="dropdown-container"> + <button refs="dropdown@toggle" class="button outline" aria-haspopup="true" aria-expanded="false">Convert Chapter</button> + <ul refs="dropdown@menu" class="dropdown-menu" role="menu"> + <li class="px-m py-s text-small text-muted"> + Are you sure you want to convert this chapter? + <br> + This cannot be as easily undone. + </li> + <li> + <form action="{{ $chapter->getUrl('/convert-to-book') }}" method="POST"> + {!! csrf_field() !!} + <button type="submit" class="text-primary text-item">{{ trans('common.confirm') }}</button> + </form> + </li> + </ul> + </div> + </div> + </div> + </div> + </div> @stop \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 37f59b970..dfda97253 100644 --- a/routes/web.php +++ b/routes/web.php @@ -132,6 +132,7 @@ Route::middleware('auth')->group(function () { Route::get('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'showCopy']); Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']); + Route::post('/books/{bookSlug}/chapter/{chapterSlug}/convert-to-book', [ChapterController::class, 'convertToBook']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ChapterExportController::class, 'html']);