From 3599a962a38692e2704804827f461997aafeba6e Mon Sep 17 00:00:00 2001 From: Lennert Daniels Date: Fri, 2 Dec 2022 13:10:57 +0100 Subject: [PATCH 01/12] search-box-cancel placement --- resources/sass/_forms.scss | 2 +- resources/views/pages/parts/template-manager.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index ef14f6221..6f1a81d12 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -454,7 +454,7 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] { &.flexible input { width: 100%; } - .search-box-cancel { + button.search-box-cancel { left: auto; right: 0; } diff --git a/resources/views/pages/parts/template-manager.blade.php b/resources/views/pages/parts/template-manager.blade.php index c209626cd..ee4467552 100644 --- a/resources/views/pages/parts/template-manager.blade.php +++ b/resources/views/pages/parts/template-manager.blade.php @@ -14,7 +14,7 @@
From 1dbc3588cf11c12fde3da0ca4cb951c55ce810d9 Mon Sep 17 00:00:00 2001 From: Lennert Daniels Date: Fri, 2 Dec 2022 18:41:59 +0100 Subject: [PATCH 02/12] Add default_template as Book setting --- app/Entities/Models/Book.php | 10 +++++- app/Http/Controllers/BookController.php | 16 +++++++++- ...2_104541_add_default_template_to_books.php | 32 +++++++++++++++++++ resources/lang/en/entities.php | 2 ++ resources/views/books/create.blade.php | 5 ++- resources/views/books/edit.blade.php | 6 +++- resources/views/books/parts/form.blade.php | 9 ++++++ .../views/entities/template-manager.blade.php | 10 ++++++ 8 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2022_12_02_104541_add_default_template_to_books.php create mode 100644 resources/views/entities/template-manager.blade.php diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index fc4556857..b84a351f8 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -27,7 +27,7 @@ class Book extends Entity implements HasCoverImage public $searchFactor = 1.2; - protected $fillable = ['name', 'description']; + protected $fillable = ['name', 'description', 'default_template']; protected $hidden = ['pivot', 'image_id', 'deleted_at']; /** @@ -78,6 +78,14 @@ class Book extends Entity implements HasCoverImage return 'cover_book'; } + /** + * Get the Page that is used as default template for newly created pages within this Book. + */ + public function defaultTemplate(): BelongsTo + { + return $this->belongsTo(Page::class, 'default_template'); + } + /** * Get all pages within this book. */ diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 14c3af1cc..9d8db27e9 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -6,6 +6,7 @@ use BookStack\Actions\ActivityQueries; use BookStack\Actions\ActivityType; use BookStack\Actions\View; use BookStack\Entities\Models\Bookshelf; +use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; @@ -79,8 +80,14 @@ class BookController extends Controller $this->setPageTitle(trans('entities.books_create')); + $templates = Page::visible() + ->where('template', '=', true) + ->orderBy('name', 'asc') + ->get(); + return view('books.create', [ 'bookshelf' => $bookshelf, + 'templates' => $templates, ]); } @@ -98,6 +105,7 @@ class BookController extends Controller 'description' => ['string', 'max:1000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'tags' => ['array'], + 'default_template' => ['nullable', 'exists:pages,id'], ]); $bookshelf = null; @@ -151,7 +159,12 @@ class BookController extends Controller $this->checkOwnablePermission('book-update', $book); $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()])); - return view('books.edit', ['book' => $book, 'current' => $book]); + $templates = Page::visible() + ->where('template', '=', true) + ->orderBy('name', 'asc') + ->get(); + + return view('books.edit', ['book' => $book, 'current' => $book, 'templates' => $templates]); } /** @@ -171,6 +184,7 @@ class BookController extends Controller 'description' => ['string', 'max:1000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'tags' => ['array'], + 'default_template' => ['nullable', 'exists:pages,id'], ]); if ($request->has('image_reset')) { diff --git a/database/migrations/2022_12_02_104541_add_default_template_to_books.php b/database/migrations/2022_12_02_104541_add_default_template_to_books.php new file mode 100644 index 000000000..755f83b5c --- /dev/null +++ b/database/migrations/2022_12_02_104541_add_default_template_to_books.php @@ -0,0 +1,32 @@ +integer('default_template')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('books', function (Blueprint $table) { + $table->dropColumn('default_template'); + }); + } +} diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index fa2586f8d..38c2f2ae3 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -328,6 +328,8 @@ return [ 'templates_replace_content' => 'Replace page content', 'templates_append_content' => 'Append to page content', 'templates_prepend_content' => 'Prepend to page content', + 'default_template' => 'Default Page Template', + 'default_template_explain' => "Assign a default template that will be used for all new pages in this book.", // Profile View 'profile_user_for_x' => 'User for :time', diff --git a/resources/views/books/create.blade.php b/resources/views/books/create.blade.php index eead4191c..6253a49bb 100644 --- a/resources/views/books/create.blade.php +++ b/resources/views/books/create.blade.php @@ -28,7 +28,10 @@

{{ trans('entities.books_create') }}

- @include('books.parts.form', ['returnLocation' => isset($bookshelf) ? $bookshelf->getUrl() : url('/books')]) + @include('books.parts.form', [ + 'templates' => $templates, + 'returnLocation' => isset($bookshelf) ? $bookshelf->getUrl() : url('/books') + ])
diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php index 180500e0a..9ec472935 100644 --- a/resources/views/books/edit.blade.php +++ b/resources/views/books/edit.blade.php @@ -18,7 +18,11 @@

{{ trans('entities.books_edit') }}

- @include('books.parts.form', ['model' => $book, 'returnLocation' => $book->getUrl()]) + @include('books.parts.form', [ + 'model' => $book, + 'templates' => $templates, + 'returnLocation' => $book->getUrl() + ])
diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index e893bcead..c6ef7d171 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -35,6 +35,15 @@ +
+ +
+ @include('entities.template-manager', ['entity' => $book ?? null, 'templates' => $templates]) +
+
+
{{ trans('common.cancel') }} diff --git a/resources/views/entities/template-manager.blade.php b/resources/views/entities/template-manager.blade.php new file mode 100644 index 000000000..fe04d9389 --- /dev/null +++ b/resources/views/entities/template-manager.blade.php @@ -0,0 +1,10 @@ +

+ {!! nl2br(e(trans('entities.default_template_explain'))) !!} +

+ + \ No newline at end of file From 99ae759effb709694ab934ae640a104e59a1a509 Mon Sep 17 00:00:00 2001 From: Lennert Daniels Date: Fri, 2 Dec 2022 18:42:58 +0100 Subject: [PATCH 03/12] Prefill new pages with book's default template --- app/Entities/Repos/PageRepo.php | 6 ++++++ app/Http/Controllers/PageController.php | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index c8eddc398..a1558b85d 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -148,6 +148,12 @@ class PageRepo $page->book_id = $parent->id; } + if ($page->book->defaultTemplate) { + $page->forceFill([ + 'html' => $page->book->defaultTemplate->html, + ]); + } + $page->save(); $page->refresh()->rebuildPermissions(); diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 9e09aed16..394147ce2 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -74,7 +74,6 @@ class PageController extends Controller $page = $this->pageRepo->getNewDraftPage($parent); $this->pageRepo->publishDraft($page, [ 'name' => $request->get('name'), - 'html' => '', ]); return redirect($page->getUrl('/edit')); From ec3b06d83f660a90036edd76e0a0c3de52645252 Mon Sep 17 00:00:00 2001 From: Lennert Daniels Date: Fri, 2 Dec 2022 18:43:51 +0100 Subject: [PATCH 04/12] Add notice to Page delete confirmation when in use as a template --- app/Http/Controllers/PageController.php | 3 +++ resources/lang/en/entities.php | 1 + resources/views/pages/delete.blade.php | 3 +++ 3 files changed, 7 insertions(+) diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 394147ce2..8b131c4f3 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\View; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\BookContents; @@ -265,11 +266,13 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-delete', $page); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()])); + $times_used_as_template = Book::where('default_template', '=', $page->id)->count(); return view('pages.delete', [ 'book' => $page->book, 'page' => $page, 'current' => $page, + 'times_used_as_template' => $times_used_as_template, ]); } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 38c2f2ae3..4af6120f8 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -192,6 +192,7 @@ return [ 'pages_delete_draft' => 'Delete Draft Page', 'pages_delete_success' => 'Page deleted', 'pages_delete_draft_success' => 'Draft page deleted', + 'pages_delete_warning_template' => '{0}|{1}Be careful: this page is used as a template for :count book.|[2,*]Be careful: this page is used as a template for :count books.', 'pages_delete_confirm' => 'Are you sure you want to delete this page?', 'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?', 'pages_editing_named' => 'Editing Page :pageName', diff --git a/resources/views/pages/delete.blade.php b/resources/views/pages/delete.blade.php index 39cd07bbb..9ce50d48b 100644 --- a/resources/views/pages/delete.blade.php +++ b/resources/views/pages/delete.blade.php @@ -19,6 +19,9 @@

{{ $page->draft ? trans('entities.pages_delete_draft') : trans('entities.pages_delete') }}

+ @if ($times_used_as_template > 0) +

{{ trans_choice('entities.pages_delete_warning_template', $times_used_as_template) }}

+ @endif
From ac519b3009e353448fc0541d21c08422d77dc57d Mon Sep 17 00:00:00 2001 From: Lennert Daniels Date: Fri, 2 Dec 2022 18:44:17 +0100 Subject: [PATCH 05/12] Guest create page: name field autofocus --- resources/views/pages/guest-create.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/pages/guest-create.blade.php b/resources/views/pages/guest-create.blade.php index d6e1cae44..11970b8b6 100644 --- a/resources/views/pages/guest-create.blade.php +++ b/resources/views/pages/guest-create.blade.php @@ -22,7 +22,7 @@
- @include('form.text', ['name' => 'name']) + @include('form.text', ['name' => 'name', 'autofocus' => true])
From d61f42a3770b9a25c1ade61342d449ff2cbe2fd1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 11 Dec 2023 12:33:20 +0000 Subject: [PATCH 06/12] Default Templates: Started review and updates from PR code --- app/Entities/Controllers/BookController.php | 48 +++++++------------ app/Entities/Controllers/PageController.php | 4 +- app/Entities/Models/Book.php | 3 +- app/Entities/Repos/PageRepo.php | 6 ++- ..._104541_add_default_template_to_books.php} | 0 lang/en/entities.php | 6 +-- resources/views/books/create.blade.php | 5 +- resources/views/books/edit.blade.php | 3 +- resources/views/books/parts/form.blade.php | 4 +- .../parts/template-selector.blade.php} | 7 ++- resources/views/pages/delete.blade.php | 4 +- 11 files changed, 39 insertions(+), 51 deletions(-) rename database/migrations/{2022_12_02_104541_add_default_template_to_books.php => 2023_12_02_104541_add_default_template_to_books.php} (100%) rename resources/views/{entities/template-manager.blade.php => books/parts/template-selector.blade.php} (58%) diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index 9b938d89a..12df935b0 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -7,7 +7,6 @@ use BookStack\Activity\ActivityType; use BookStack\Activity\Models\View; use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; @@ -25,15 +24,11 @@ use Throwable; class BookController extends Controller { - protected BookRepo $bookRepo; - protected ShelfContext $shelfContext; - protected ReferenceFetcher $referenceFetcher; - - public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo, ReferenceFetcher $referenceFetcher) - { - $this->bookRepo = $bookRepo; - $this->shelfContext = $entityContextManager; - $this->referenceFetcher = $referenceFetcher; + public function __construct( + protected ShelfContext $shelfContext, + protected BookRepo $bookRepo, + protected ReferenceFetcher $referenceFetcher + ) { } /** @@ -82,14 +77,8 @@ class BookController extends Controller $this->setPageTitle(trans('entities.books_create')); - $templates = Page::visible() - ->where('template', '=', true) - ->orderBy('name', 'asc') - ->get(); - return view('books.create', [ 'bookshelf' => $bookshelf, - 'templates' => $templates, ]); } @@ -103,11 +92,11 @@ class BookController extends Controller { $this->checkPermission('book-create-all'); $validated = $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'image' => array_merge(['nullable'], $this->getImageValidationRules()), - 'tags' => ['array'], - 'default_template' => ['nullable', 'exists:pages,id'], + 'name' => ['required', 'string', 'max:255'], + 'description' => ['string', 'max:1000'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'tags' => ['array'], + 'default_template' => ['nullable', 'integer'], ]); $bookshelf = null; @@ -162,12 +151,7 @@ class BookController extends Controller $this->checkOwnablePermission('book-update', $book); $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()])); - $templates = Page::visible() - ->where('template', '=', true) - ->orderBy('name', 'asc') - ->get(); - - return view('books.edit', ['book' => $book, 'current' => $book, 'templates' => $templates]); + return view('books.edit', ['book' => $book, 'current' => $book]); } /** @@ -183,11 +167,11 @@ class BookController extends Controller $this->checkOwnablePermission('book-update', $book); $validated = $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'image' => array_merge(['nullable'], $this->getImageValidationRules()), - 'tags' => ['array'], - 'default_template' => ['nullable', 'exists:pages,id'], + 'name' => ['required', 'string', 'max:255'], + 'description' => ['string', 'max:1000'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'tags' => ['array'], + 'default_template' => ['nullable', 'integer'], ]); if ($request->has('image_reset')) { diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index ad75448b3..11f19f72f 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -259,13 +259,13 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-delete', $page); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()])); - $times_used_as_template = Book::where('default_template', '=', $page->id)->count(); + $usedAsTemplate = Book::query()->where('default_template', '=', $page->id)->count() > 0; return view('pages.delete', [ 'book' => $page->book, 'page' => $page, 'current' => $page, - 'times_used_as_template' => $times_used_as_template, + 'usedAsTemplate' => $usedAsTemplate, ]); } diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 8584e755e..19aba0525 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -20,6 +20,7 @@ use Illuminate\Support\Collection; * @property \Illuminate\Database\Eloquent\Collection $pages * @property \Illuminate\Database\Eloquent\Collection $directPages * @property \Illuminate\Database\Eloquent\Collection $shelves + * @property ?Page $defaultTemplate */ class Book extends Entity implements HasCoverImage { @@ -27,7 +28,7 @@ class Book extends Entity implements HasCoverImage public $searchFactor = 1.2; - protected $fillable = ['name', 'description', 'default_template']; + protected $fillable = ['name', 'description']; protected $hidden = ['pivot', 'image_id', 'deleted_at']; /** diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 5269a0bcc..9a183469b 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -136,9 +136,11 @@ class PageRepo $page->book_id = $parent->id; } - if ($page->book->defaultTemplate) { + $defaultTemplate = $page->book->defaultTemplate; + if ($defaultTemplate && userCan('view', $defaultTemplate)) { $page->forceFill([ - 'html' => $page->book->defaultTemplate->html, + 'html' => $defaultTemplate->html, + 'markdown' => $defaultTemplate->markdown, ]); } diff --git a/database/migrations/2022_12_02_104541_add_default_template_to_books.php b/database/migrations/2023_12_02_104541_add_default_template_to_books.php similarity index 100% rename from database/migrations/2022_12_02_104541_add_default_template_to_books.php rename to database/migrations/2023_12_02_104541_add_default_template_to_books.php diff --git a/lang/en/entities.php b/lang/en/entities.php index e4c67f5ca..ee612b7ba 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -132,6 +132,8 @@ return [ 'books_edit_named' => 'Edit Book :bookName', 'books_form_book_name' => 'Book Name', 'books_save' => 'Save Book', + 'books_default_template' => 'Default Page Template', + 'books_default_template_explain' => 'Assign a default template that will be used for all new pages in this book. Keep in mind this will only be used if the page creator has view access to those chosen template.', 'books_permissions' => 'Book Permissions', 'books_permissions_updated' => 'Book Permissions Updated', 'books_empty_contents' => 'No pages or chapters have been created for this book.', @@ -204,7 +206,7 @@ return [ 'pages_delete_draft' => 'Delete Draft Page', 'pages_delete_success' => 'Page deleted', 'pages_delete_draft_success' => 'Draft page deleted', - 'pages_delete_warning_template' => '{0}|{1}Be careful: this page is used as a template for :count book.|[2,*]Be careful: this page is used as a template for :count books.', + 'pages_delete_warning_template' => 'This page is in active use as a book default page template. These books will no longer have a page default template assigned after this page is deleted.', 'pages_delete_confirm' => 'Are you sure you want to delete this page?', 'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?', 'pages_editing_named' => 'Editing Page :pageName', @@ -351,8 +353,6 @@ return [ 'templates_replace_content' => 'Replace page content', 'templates_append_content' => 'Append to page content', 'templates_prepend_content' => 'Prepend to page content', - 'default_template' => 'Default Page Template', - 'default_template_explain' => "Assign a default template that will be used for all new pages in this book.", // Profile View 'profile_user_for_x' => 'User for :time', diff --git a/resources/views/books/create.blade.php b/resources/views/books/create.blade.php index 6253a49bb..318abfcf1 100644 --- a/resources/views/books/create.blade.php +++ b/resources/views/books/create.blade.php @@ -27,10 +27,9 @@

{{ trans('entities.books_create') }}

-
+ @include('books.parts.form', [ - 'templates' => $templates, - 'returnLocation' => isset($bookshelf) ? $bookshelf->getUrl() : url('/books') + 'returnLocation' => $bookshelf?->getUrl() ?? url('/books') ])
diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php index 9ec472935..6efd1caea 100644 --- a/resources/views/books/edit.blade.php +++ b/resources/views/books/edit.blade.php @@ -19,8 +19,7 @@
@include('books.parts.form', [ - 'model' => $book, - 'templates' => $templates, + 'model' => $book, 'returnLocation' => $book->getUrl() ])
diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index 9b66b8ac8..a6b0eade2 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -37,10 +37,10 @@
- @include('entities.template-manager', ['entity' => $book ?? null, 'templates' => $templates]) + @include('books.parts.template-selector', ['entity' => $book ?? null, 'templates' => []])
diff --git a/resources/views/entities/template-manager.blade.php b/resources/views/books/parts/template-selector.blade.php similarity index 58% rename from resources/views/entities/template-manager.blade.php rename to resources/views/books/parts/template-selector.blade.php index fe04d9389..90c5e421b 100644 --- a/resources/views/entities/template-manager.blade.php +++ b/resources/views/books/parts/template-selector.blade.php @@ -1,5 +1,5 @@

- {!! nl2br(e(trans('entities.default_template_explain'))) !!} + {{ trans('entities.books_default_template_explain') }}

\ No newline at end of file + + + +@include('settings.parts.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')]) \ No newline at end of file diff --git a/resources/views/pages/delete.blade.php b/resources/views/pages/delete.blade.php index 9ce50d48b..40125dfe2 100644 --- a/resources/views/pages/delete.blade.php +++ b/resources/views/pages/delete.blade.php @@ -19,8 +19,8 @@

{{ $page->draft ? trans('entities.pages_delete_draft') : trans('entities.pages_delete') }}

- @if ($times_used_as_template > 0) -

{{ trans_choice('entities.pages_delete_warning_template', $times_used_as_template) }}

+ @if($usedAsTemplate) +

{{ trans('entities.pages_delete_warning_template') }}

@endif
From 7ebe7d4e58f4555d6a9a253f976e22af9add7dec Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 11 Dec 2023 15:55:43 +0000 Subject: [PATCH 07/12] Default templates: Added page picker and working forms - Adapted existing page picker to be usable elsewhere. - Added endpoint for getting templates for entity picker. - Added search template filter to support above. - Updated book save handling to check/validate submitted template. - Allows non-visible pages to flow through the save process, if not being changed. - Updated page deletes to handle removal of default usage on books. - Tweaked wording and form styles to suit. - Updated migration to explicity reflect default value. --- app/Entities/Controllers/PageController.php | 2 + app/Entities/Models/Book.php | 1 + app/Entities/Repos/BookRepo.php | 49 ++++++++++++++----- app/Entities/Tools/TrashCan.php | 4 ++ app/Search/SearchController.php | 27 ++++++++++ app/Search/SearchOptions.php | 8 +++ app/Search/SearchRunner.php | 9 +++- ...2_104541_add_default_template_to_books.php | 2 +- lang/en/entities.php | 5 +- resources/js/components/entity-selector.js | 6 +-- resources/sass/_layout.scss | 4 ++ resources/views/books/parts/form.blade.php | 18 ++++++- .../books/parts/template-selector.blade.php | 13 ----- .../views/entities/selector-popup.blade.php | 2 +- resources/views/entities/selector.blade.php | 3 +- .../parts => form}/page-picker.blade.php | 4 +- resources/views/pages/delete.blade.php | 2 +- .../views/settings/customization.blade.php | 2 +- routes/web.php | 1 + 19 files changed, 121 insertions(+), 41 deletions(-) delete mode 100644 resources/views/books/parts/template-selector.blade.php rename resources/views/{settings/parts => form}/page-picker.blade.php (87%) diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index 11f19f72f..d92934123 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -279,11 +279,13 @@ class PageController extends Controller $page = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-update', $page); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()])); + $usedAsTemplate = Book::query()->where('default_template', '=', $page->id)->count() > 0; return view('pages.delete', [ 'book' => $page->book, 'page' => $page, 'current' => $page, + 'usedAsTemplate' => $usedAsTemplate, ]); } diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 19aba0525..faae276a5 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -15,6 +15,7 @@ use Illuminate\Support\Collection; * * @property string $description * @property int $image_id + * @property ?int $default_template * @property Image|null $cover * @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection $pages diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 737caa70b..b46218fe0 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -5,6 +5,7 @@ namespace BookStack\Entities\Repos; use BookStack\Activity\ActivityType; use BookStack\Activity\TagRepo; use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\TrashCan; use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\NotFoundException; @@ -17,18 +18,11 @@ use Illuminate\Support\Collection; class BookRepo { - protected $baseRepo; - protected $tagRepo; - protected $imageRepo; - - /** - * BookRepo constructor. - */ - public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo) - { - $this->baseRepo = $baseRepo; - $this->tagRepo = $tagRepo; - $this->imageRepo = $imageRepo; + public function __construct( + protected BaseRepo $baseRepo, + protected TagRepo $tagRepo, + protected ImageRepo $imageRepo + ) { } /** @@ -104,6 +98,10 @@ class BookRepo { $this->baseRepo->update($book, $input); + if (array_key_exists('default_template', $input)) { + $this->updateBookDefaultTemplate($book, intval($input['default_template'])); + } + if (array_key_exists('image', $input)) { $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null); } @@ -113,6 +111,33 @@ class BookRepo return $book; } + /** + * Update the default page template used for this book. + * Checks that, if changing, the provided value is a valid template and the user + * has visibility of the provided page template id. + */ + protected function updateBookDefaultTemplate(Book $book, int $templateId): void + { + $changing = $templateId !== intval($book->default_template); + if (!$changing) { + return; + } + + if ($templateId === 0) { + $book->default_template = null; + $book->save(); + return; + } + + $templateExists = Page::query()->visible() + ->where('template', '=', true) + ->where('id', '=', $templateId) + ->exists(); + + $book->default_template = $templateExists ? $templateId : null; + $book->save(); + } + /** * Update the given book's cover image, or clear it. * diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 08276230c..b0c452456 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -202,6 +202,10 @@ class TrashCan $attachmentService->deleteFile($attachment); } + // Remove book template usages + Book::query()->where('default_template', '=', $page->id) + ->update(['default_template' => null]); + $page->forceDelete(); return 1; diff --git a/app/Search/SearchController.php b/app/Search/SearchController.php index 09a67f2b5..6cf12a579 100644 --- a/app/Search/SearchController.php +++ b/app/Search/SearchController.php @@ -2,6 +2,7 @@ namespace BookStack\Search; +use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\Popular; use BookStack\Entities\Tools\SiblingFetcher; use BookStack\Http\Controller; @@ -82,6 +83,32 @@ class SearchController extends Controller return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]); } + /** + * Search for a list of templates to choose from. + */ + public function templatesForSelector(Request $request) + { + $searchTerm = $request->get('term', false); + + if ($searchTerm !== false) { + $searchOptions = SearchOptions::fromString($searchTerm); + $searchOptions->setFilter('is_template'); + $entities = $this->searchRunner->searchEntities($searchOptions, 'page', 1, 20)['results']; + } else { + $entities = Page::visible() + ->where('template', '=', true) + ->where('draft', '=', false) + ->orderBy('updated_at', 'desc') + ->take(20) + ->get(Page::$listAttributes); + } + + return view('search.parts.entity-selector-list', [ + 'entities' => $entities, + 'permission' => 'view' + ]); + } + /** * Search for a list of entities and return a partial HTML response of matching entities * to be used as a result preview suggestion list for global system searches. diff --git a/app/Search/SearchOptions.php b/app/Search/SearchOptions.php index d38fc8d57..fffa03db0 100644 --- a/app/Search/SearchOptions.php +++ b/app/Search/SearchOptions.php @@ -170,6 +170,14 @@ class SearchOptions return $parsed; } + /** + * Set the value of a specific filter in the search options. + */ + public function setFilter(string $filterName, string $filterValue = ''): void + { + $this->filters[$filterName] = $filterValue; + } + /** * Encode this instance to a search string. */ diff --git a/app/Search/SearchRunner.php b/app/Search/SearchRunner.php index fc36cb816..aac9d1000 100644 --- a/app/Search/SearchRunner.php +++ b/app/Search/SearchRunner.php @@ -58,7 +58,7 @@ class SearchRunner $entityTypesToSearch = $entityTypes; if ($entityType !== 'all') { - $entityTypesToSearch = $entityType; + $entityTypesToSearch = [$entityType]; } elseif (isset($searchOpts->filters['type'])) { $entityTypesToSearch = explode('|', $searchOpts->filters['type']); } @@ -469,6 +469,13 @@ class SearchRunner }); } + protected function filterIsTemplate(EloquentBuilder $query, Entity $model, $input) + { + if ($model instanceof Page) { + $query->where('template', '=', true); + } + } + protected function filterSortBy(EloquentBuilder $query, Entity $model, $input) { $functionName = Str::camel('sort_by_' . $input); diff --git a/database/migrations/2023_12_02_104541_add_default_template_to_books.php b/database/migrations/2023_12_02_104541_add_default_template_to_books.php index 755f83b5c..913361dcb 100644 --- a/database/migrations/2023_12_02_104541_add_default_template_to_books.php +++ b/database/migrations/2023_12_02_104541_add_default_template_to_books.php @@ -14,7 +14,7 @@ class AddDefaultTemplateToBooks extends Migration public function up() { Schema::table('books', function (Blueprint $table) { - $table->integer('default_template')->nullable(); + $table->integer('default_template')->nullable()->default(null); }); } diff --git a/lang/en/entities.php b/lang/en/entities.php index ee612b7ba..354eee42e 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -133,7 +133,8 @@ return [ 'books_form_book_name' => 'Book Name', 'books_save' => 'Save Book', 'books_default_template' => 'Default Page Template', - 'books_default_template_explain' => 'Assign a default template that will be used for all new pages in this book. Keep in mind this will only be used if the page creator has view access to those chosen template.', + 'books_default_template_explain' => 'Assign a page template that will be used as the default content for all new pages in this book. Keep in mind this will only be used if the page creator has view access to those chosen template page.', + 'books_default_template_select' => 'Select a template page', 'books_permissions' => 'Book Permissions', 'books_permissions_updated' => 'Book Permissions Updated', 'books_empty_contents' => 'No pages or chapters have been created for this book.', @@ -206,7 +207,7 @@ return [ 'pages_delete_draft' => 'Delete Draft Page', 'pages_delete_success' => 'Page deleted', 'pages_delete_draft_success' => 'Draft page deleted', - 'pages_delete_warning_template' => 'This page is in active use as a book default page template. These books will no longer have a page default template assigned after this page is deleted.', + 'pages_delete_warning_template' => 'This page is in active use as a book default page template. These books will no longer have a default page template assigned after this page is deleted.', 'pages_delete_confirm' => 'Are you sure you want to delete this page?', 'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?', 'pages_editing_named' => 'Editing Page :pageName', diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index 9cda35874..b12eeb402 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -10,6 +10,7 @@ export class EntitySelector extends Component { this.elem = this.$el; this.entityTypes = this.$opts.entityTypes || 'page,book,chapter'; this.entityPermission = this.$opts.entityPermission || 'view'; + this.searchEndpoint = this.$opts.searchEndpoint || '/search/entity-selector'; this.input = this.$refs.input; this.searchInput = this.$refs.search; @@ -18,7 +19,6 @@ export class EntitySelector extends Component { this.search = ''; this.lastClick = 0; - this.selectedItemData = null; this.setupListeners(); this.showLoading(); @@ -110,7 +110,7 @@ export class EntitySelector extends Component { } searchUrl() { - return `/search/entity-selector?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`; + return `${this.searchEndpoint}?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`; } searchEntities(searchTerm) { @@ -153,7 +153,6 @@ export class EntitySelector extends Component { if (isSelected) { item.classList.add('selected'); - this.selectedItemData = data; } else { window.$events.emit('entity-select-change', null); } @@ -177,7 +176,6 @@ export class EntitySelector extends Component { for (const selectedElem of selected) { selectedElem.classList.remove('selected', 'primary-background'); } - this.selectedItemData = null; } } diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index d157ffdc3..94a36ecba 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -266,6 +266,10 @@ body.flexbox { display: none !important; } +.overflow-hidden { + overflow: hidden; +} + .fill-height { height: 100%; } diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index a6b0eade2..b16468a09 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -40,11 +40,25 @@
- @include('books.parts.template-selector', ['entity' => $book ?? null, 'templates' => []]) +
+

+ {{ trans('entities.books_default_template_explain') }} +

+ + + @include('form.page-picker', [ + 'name' => 'default_template', + 'placeholder' => trans('entities.books_default_template_select'), + 'value' => $book?->default_template ?? null, + ]) +
+
{{ trans('common.cancel') }} -
\ No newline at end of file +
+ +@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates']) \ No newline at end of file diff --git a/resources/views/books/parts/template-selector.blade.php b/resources/views/books/parts/template-selector.blade.php deleted file mode 100644 index 90c5e421b..000000000 --- a/resources/views/books/parts/template-selector.blade.php +++ /dev/null @@ -1,13 +0,0 @@ -

- {{ trans('entities.books_default_template_explain') }} -

- - - - -@include('settings.parts.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')]) \ No newline at end of file diff --git a/resources/views/entities/selector-popup.blade.php b/resources/views/entities/selector-popup.blade.php index c896b50b5..d4c941e9a 100644 --- a/resources/views/entities/selector-popup.blade.php +++ b/resources/views/entities/selector-popup.blade.php @@ -7,7 +7,7 @@
@include('entities.selector', ['name' => 'entity-selector'])
diff --git a/resources/views/entities/selector.blade.php b/resources/views/entities/selector.blade.php index a9f5b932c..c1280cfb2 100644 --- a/resources/views/entities/selector.blade.php +++ b/resources/views/entities/selector.blade.php @@ -3,7 +3,8 @@ refs="entity-selector-popup@selector" class="entity-selector {{$selectorSize ?? ''}}" option:entity-selector:entity-types="{{ $entityTypes ?? 'book,chapter,page' }}" - option:entity-selector:entity-permission="{{ $entityPermission ?? 'view' }}"> + option:entity-selector:entity-permission="{{ $entityPermission ?? 'view' }}" + option:entity-selector:search-endpoint="{{ $selectorEndpoint ?? '/search/entity-selector' }}">
@include('common.loading-icon')
diff --git a/resources/views/settings/parts/page-picker.blade.php b/resources/views/form/page-picker.blade.php similarity index 87% rename from resources/views/settings/parts/page-picker.blade.php rename to resources/views/form/page-picker.blade.php index d599a19ab..90ce75676 100644 --- a/resources/views/settings/parts/page-picker.blade.php +++ b/resources/views/form/page-picker.blade.php @@ -1,9 +1,9 @@ {{--Depends on entity selector popup--}}
-
+
diff --git a/resources/views/pages/delete.blade.php b/resources/views/pages/delete.blade.php index 40125dfe2..a9c4b73ad 100644 --- a/resources/views/pages/delete.blade.php +++ b/resources/views/pages/delete.blade.php @@ -20,7 +20,7 @@

{{ $page->draft ? trans('entities.pages_delete_draft') : trans('entities.pages_delete') }}

@if($usedAsTemplate) -

{{ trans('entities.pages_delete_warning_template') }}

+

{{ trans('entities.pages_delete_warning_template') }}

@endif
diff --git a/resources/views/settings/customization.blade.php b/resources/views/settings/customization.blade.php index be99cc254..7112ebcff 100644 --- a/resources/views/settings/customization.blade.php +++ b/resources/views/settings/customization.blade.php @@ -133,7 +133,7 @@
diff --git a/routes/web.php b/routes/web.php index 8fc90ee54..4620cd08b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -182,6 +182,7 @@ Route::middleware('auth')->group(function () { Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']); Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']); Route::get('/search/entity-selector', [SearchController::class, 'searchForSelector']); + Route::get('/search/entity-selector-templates', [SearchController::class, 'templatesForSelector']); Route::get('/search/suggest', [SearchController::class, 'searchSuggestions']); // User Search From 4017048555efd20cced7c6c5feac28b6131ccf2f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 Dec 2023 12:14:00 +0000 Subject: [PATCH 08/12] Page Templates: Changed template field name, added API support --- .../Controllers/BookApiController.php | 14 +++++++------ app/Entities/Controllers/BookController.php | 20 +++++++++---------- app/Entities/Controllers/PageController.php | 4 ++-- app/Entities/Models/Book.php | 4 ++-- app/Entities/Repos/BookRepo.php | 11 +++++----- app/Entities/Tools/TrashCan.php | 4 ++-- ...2_104541_add_default_template_to_books.php | 4 ++-- dev/api/requests/books-create.json | 7 ++++++- dev/api/requests/books-update.json | 6 +++++- dev/api/responses/books-create.json | 1 + dev/api/responses/books-read.json | 1 + dev/api/responses/books-update.json | 9 +++++---- resources/views/books/parts/form.blade.php | 4 ++-- tests/Api/BooksApiTest.php | 6 ++++++ tests/Helpers/EntityProvider.php | 9 +++++++++ 15 files changed, 67 insertions(+), 37 deletions(-) diff --git a/app/Entities/Controllers/BookApiController.php b/app/Entities/Controllers/BookApiController.php index cb67184a0..41ff11dde 100644 --- a/app/Entities/Controllers/BookApiController.php +++ b/app/Entities/Controllers/BookApiController.php @@ -14,11 +14,9 @@ use Illuminate\Validation\ValidationException; class BookApiController extends ApiController { - protected BookRepo $bookRepo; - - public function __construct(BookRepo $bookRepo) - { - $this->bookRepo = $bookRepo; + public function __construct( + protected BookRepo $bookRepo + ) { } /** @@ -58,7 +56,9 @@ class BookApiController extends ApiController */ public function read(string $id) { - $book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($id); + $book = Book::visible() + ->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy']) + ->findOrFail($id); $contents = (new BookContents($book))->getTree(true, false)->all(); $contentsApiData = (new ApiEntityListFormatter($contents)) @@ -116,12 +116,14 @@ class BookApiController extends ApiController 'description' => ['string', 'max:1000'], 'tags' => ['array'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'default_template_id' => ['nullable', 'integer'], ], 'update' => [ 'name' => ['string', 'min:1', 'max:255'], 'description' => ['string', 'max:1000'], 'tags' => ['array'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'default_template_id' => ['nullable', 'integer'], ], ]; } diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index 12df935b0..faa578893 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -92,11 +92,11 @@ class BookController extends Controller { $this->checkPermission('book-create-all'); $validated = $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'image' => array_merge(['nullable'], $this->getImageValidationRules()), - 'tags' => ['array'], - 'default_template' => ['nullable', 'integer'], + 'name' => ['required', 'string', 'max:255'], + 'description' => ['string', 'max:1000'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'tags' => ['array'], + 'default_template_id' => ['nullable', 'integer'], ]); $bookshelf = null; @@ -167,11 +167,11 @@ class BookController extends Controller $this->checkOwnablePermission('book-update', $book); $validated = $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'image' => array_merge(['nullable'], $this->getImageValidationRules()), - 'tags' => ['array'], - 'default_template' => ['nullable', 'integer'], + 'name' => ['required', 'string', 'max:255'], + 'description' => ['string', 'max:1000'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'tags' => ['array'], + 'default_template_id' => ['nullable', 'integer'], ]); if ($request->has('image_reset')) { diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index d92934123..0a3e76daa 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -259,7 +259,7 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-delete', $page); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()])); - $usedAsTemplate = Book::query()->where('default_template', '=', $page->id)->count() > 0; + $usedAsTemplate = Book::query()->where('default_template_id', '=', $page->id)->count() > 0; return view('pages.delete', [ 'book' => $page->book, @@ -279,7 +279,7 @@ class PageController extends Controller $page = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-update', $page); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()])); - $usedAsTemplate = Book::query()->where('default_template', '=', $page->id)->count() > 0; + $usedAsTemplate = Book::query()->where('default_template_id', '=', $page->id)->count() > 0; return view('pages.delete', [ 'book' => $page->book, diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index faae276a5..ee9a7f447 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -15,7 +15,7 @@ use Illuminate\Support\Collection; * * @property string $description * @property int $image_id - * @property ?int $default_template + * @property ?int $default_template_id * @property Image|null $cover * @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection $pages @@ -78,7 +78,7 @@ class Book extends Entity implements HasCoverImage */ public function defaultTemplate(): BelongsTo { - return $this->belongsTo(Page::class, 'default_template'); + return $this->belongsTo(Page::class, 'default_template_id'); } /** diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index b46218fe0..03e1118b1 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -86,6 +86,7 @@ class BookRepo $book = new Book(); $this->baseRepo->create($book, $input); $this->baseRepo->updateCoverImage($book, $input['image'] ?? null); + $this->updateBookDefaultTemplate($book, intval($input['default_template_id'] ?? null)); Activity::add(ActivityType::BOOK_CREATE, $book); return $book; @@ -98,8 +99,8 @@ class BookRepo { $this->baseRepo->update($book, $input); - if (array_key_exists('default_template', $input)) { - $this->updateBookDefaultTemplate($book, intval($input['default_template'])); + if (array_key_exists('default_template_id', $input)) { + $this->updateBookDefaultTemplate($book, intval($input['default_template_id'])); } if (array_key_exists('image', $input)) { @@ -118,13 +119,13 @@ class BookRepo */ protected function updateBookDefaultTemplate(Book $book, int $templateId): void { - $changing = $templateId !== intval($book->default_template); + $changing = $templateId !== intval($book->default_template_id); if (!$changing) { return; } if ($templateId === 0) { - $book->default_template = null; + $book->default_template_id = null; $book->save(); return; } @@ -134,7 +135,7 @@ class BookRepo ->where('id', '=', $templateId) ->exists(); - $book->default_template = $templateExists ? $templateId : null; + $book->default_template_id = $templateExists ? $templateId : null; $book->save(); } diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index b0c452456..b25103985 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -203,8 +203,8 @@ class TrashCan } // Remove book template usages - Book::query()->where('default_template', '=', $page->id) - ->update(['default_template' => null]); + Book::query()->where('default_template_id', '=', $page->id) + ->update(['default_template_id' => null]); $page->forceDelete(); diff --git a/database/migrations/2023_12_02_104541_add_default_template_to_books.php b/database/migrations/2023_12_02_104541_add_default_template_to_books.php index 913361dcb..c23bebc2e 100644 --- a/database/migrations/2023_12_02_104541_add_default_template_to_books.php +++ b/database/migrations/2023_12_02_104541_add_default_template_to_books.php @@ -14,7 +14,7 @@ class AddDefaultTemplateToBooks extends Migration public function up() { Schema::table('books', function (Blueprint $table) { - $table->integer('default_template')->nullable()->default(null); + $table->integer('default_template_id')->nullable()->default(null); }); } @@ -26,7 +26,7 @@ class AddDefaultTemplateToBooks extends Migration public function down() { Schema::table('books', function (Blueprint $table) { - $table->dropColumn('default_template'); + $table->dropColumn('default_template_id'); }); } } diff --git a/dev/api/requests/books-create.json b/dev/api/requests/books-create.json index 4a6626619..2a38dba83 100644 --- a/dev/api/requests/books-create.json +++ b/dev/api/requests/books-create.json @@ -1,4 +1,9 @@ { "name": "My own book", - "description": "This is my own little book" + "description": "This is my own little book", + "default_template_id": 12, + "tags": [ + {"name": "Category", "value": "Top Content"}, + {"name": "Rating", "value": "Highest"} + ] } \ No newline at end of file diff --git a/dev/api/requests/books-update.json b/dev/api/requests/books-update.json index fc67d5fcc..c026b7b49 100644 --- a/dev/api/requests/books-update.json +++ b/dev/api/requests/books-update.json @@ -1,4 +1,8 @@ { "name": "My updated book", - "description": "This is my book with updated details" + "description": "This is my book with updated details", + "default_template_id": 12, + "tags": [ + {"name": "Subject", "value": "Updates"} + ] } \ No newline at end of file diff --git a/dev/api/responses/books-create.json b/dev/api/responses/books-create.json index 12a3e9e9f..773879125 100644 --- a/dev/api/responses/books-create.json +++ b/dev/api/responses/books-create.json @@ -6,6 +6,7 @@ "created_by": 1, "updated_by": 1, "owned_by": 1, + "default_template_id": 12, "updated_at": "2020-01-12T14:05:11.000000Z", "created_at": "2020-01-12T14:05:11.000000Z" } \ No newline at end of file diff --git a/dev/api/responses/books-read.json b/dev/api/responses/books-read.json index 3744445d0..21e1829b8 100644 --- a/dev/api/responses/books-read.json +++ b/dev/api/responses/books-read.json @@ -20,6 +20,7 @@ "name": "Admin", "slug": "admin" }, + "default_template_id": null, "contents": [ { "id": 50, diff --git a/dev/api/responses/books-update.json b/dev/api/responses/books-update.json index 7d3d6735e..f69677c4a 100644 --- a/dev/api/responses/books-update.json +++ b/dev/api/responses/books-update.json @@ -1,11 +1,12 @@ { "id": 16, - "name": "My own book", - "slug": "my-own-book", - "description": "This is my own little book - updated", + "name": "My updated book", + "slug": "my-updated-book", + "description": "This is my book with updated details", "created_at": "2020-01-12T14:09:59.000000Z", "updated_at": "2020-01-12T14:16:10.000000Z", "created_by": 1, "updated_by": 1, - "owned_by": 1 + "owned_by": 1, + "default_template_id": 12 } \ No newline at end of file diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index b16468a09..b4ca2fba5 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -47,9 +47,9 @@ @include('form.page-picker', [ - 'name' => 'default_template', + 'name' => 'default_template_id', 'placeholder' => trans('entities.books_default_template_select'), - 'value' => $book?->default_template ?? null, + 'value' => $book?->default_template_id ?? null, ])
diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php index 326304d6f..c648faaf2 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -31,13 +31,16 @@ class BooksApiTest extends TestCase public function test_create_endpoint() { $this->actingAsApiEditor(); + $templatePage = $this->entities->templatePage(); $details = [ 'name' => 'My API book', 'description' => 'A book created via the API', + 'default_template_id' => $templatePage->id, ]; $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(200); + $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first(); $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug])); $this->assertActivityExists('book_create', $newItem); @@ -83,6 +86,7 @@ class BooksApiTest extends TestCase 'owned_by' => [ 'name' => $book->ownedBy->name, ], + 'default_template_id' => null, ]); } @@ -121,9 +125,11 @@ class BooksApiTest extends TestCase { $this->actingAsApiEditor(); $book = $this->entities->book(); + $templatePage = $this->entities->templatePage(); $details = [ 'name' => 'My updated API book', 'description' => 'A book created via the API', + 'default_template_id' => $templatePage->id, ]; $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details); diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php index 3cb8c44d3..982063421 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -53,6 +53,15 @@ class EntityProvider return $this->page(fn(Builder $query) => $query->where('chapter_id', '=', 0)); } + public function templatePage(): Page + { + $page = $this->page(); + $page->template = true; + $page->save(); + + return $page; + } + /** * Get an un-fetched chapter from the system. */ From d75eb067774d83aee63cc13abc36b0b918db67fc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 Dec 2023 15:04:40 +0000 Subject: [PATCH 09/12] Default templates: Added tests to cover functionality Included new helper in Test PermissionProvider to set app to public, since that's a common test scenario. --- tests/Entity/BookDefaultTemplateTest.php | 170 +++++++++++++++++++++++ tests/Helpers/PermissionsProvider.php | 11 +- 2 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 tests/Entity/BookDefaultTemplateTest.php diff --git a/tests/Entity/BookDefaultTemplateTest.php b/tests/Entity/BookDefaultTemplateTest.php new file mode 100644 index 000000000..f23ba4a8e --- /dev/null +++ b/tests/Entity/BookDefaultTemplateTest.php @@ -0,0 +1,170 @@ +entities->templatePage(); + $details = [ + 'name' => 'My book with default template', + 'default_template_id' => $templatePage->id, + ]; + + $this->asEditor()->post('/books', $details); + $this->assertDatabaseHas('books', $details); + } + + public function test_updating_book_with_default_template() + { + $book = $this->entities->book(); + $templatePage = $this->entities->templatePage(); + + $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => strval($templatePage->id)]); + $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => $templatePage->id]); + + $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => '']); + $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]); + } + + public function test_default_template_cannot_be_set_if_not_a_template() + { + $book = $this->entities->book(); + $page = $this->entities->page(); + $this->assertFalse($page->template); + + $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $page->id]); + $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]); + } + + public function test_default_template_cannot_be_set_if_not_have_access() + { + $book = $this->entities->book(); + $templatePage = $this->entities->templatePage(); + $this->permissions->disableEntityInheritedPermissions($templatePage); + + $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $templatePage->id]); + $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]); + } + + public function test_inaccessible_default_template_can_be_set_if_unchanged() + { + $templatePage = $this->entities->templatePage(); + $book = $this->bookUsingDefaultTemplate($templatePage); + $this->permissions->disableEntityInheritedPermissions($templatePage); + + $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $templatePage->id]); + $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => $templatePage->id]); + } + + public function test_default_page_template_option_shows_on_book_form() + { + $templatePage = $this->entities->templatePage(); + $book = $this->bookUsingDefaultTemplate($templatePage); + + $resp = $this->asEditor()->get($book->getUrl('/edit')); + $this->withHtml($resp)->assertElementExists('input[name="default_template_id"][value="' . $templatePage->id . '"]'); + } + + public function test_default_page_template_option_only_shows_template_name_if_visible() + { + $templatePage = $this->entities->templatePage(); + $book = $this->bookUsingDefaultTemplate($templatePage); + + $resp = $this->asEditor()->get($book->getUrl('/edit')); + $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}"); + + $this->permissions->disableEntityInheritedPermissions($templatePage); + + $resp = $this->asEditor()->get($book->getUrl('/edit')); + $this->withHtml($resp)->assertElementNotContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}"); + $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}"); + } + + public function test_creating_book_page_uses_default_template() + { + $templatePage = $this->entities->templatePage(); + $templatePage->forceFill(['html' => '

My template page

', 'markdown' => '# My template page'])->save(); + $book = $this->bookUsingDefaultTemplate($templatePage); + + $this->asEditor()->get($book->getUrl('/create-page')); + $latestPage = $book->pages() + ->where('draft', '=', true) + ->where('template', '=', false) + ->latest()->first(); + + $this->assertEquals('

My template page

', $latestPage->html); + $this->assertEquals('# My template page', $latestPage->markdown); + } + + public function test_creating_chapter_page_uses_default_template() + { + $templatePage = $this->entities->templatePage(); + $templatePage->forceFill(['html' => '

My template page in chapter

', 'markdown' => '# My template page in chapter'])->save(); + $book = $this->bookUsingDefaultTemplate($templatePage); + $chapter = $book->chapters()->first(); + + $this->asEditor()->get($chapter->getUrl('/create-page')); + $latestPage = $chapter->pages() + ->where('draft', '=', true) + ->where('template', '=', false) + ->latest()->first(); + + $this->assertEquals('

My template page in chapter

', $latestPage->html); + $this->assertEquals('# My template page in chapter', $latestPage->markdown); + } + + public function test_creating_book_page_as_guest_uses_default_template() + { + $templatePage = $this->entities->templatePage(); + $templatePage->forceFill(['html' => '

My template page

', 'markdown' => '# My template page'])->save(); + $book = $this->bookUsingDefaultTemplate($templatePage); + $guest = $this->users->guest(); + + $this->permissions->makeAppPublic(); + $this->permissions->grantUserRolePermissions($guest, ['page-create-all', 'page-update-all']); + + $resp = $this->post($book->getUrl('/create-guest-page'), [ + 'name' => 'My guest page with template' + ]); + $latestPage = $book->pages() + ->where('draft', '=', false) + ->where('template', '=', false) + ->where('created_by', '=', $guest->id) + ->latest()->first(); + + $this->assertEquals('

My template page

', $latestPage->html); + $this->assertEquals('# My template page', $latestPage->markdown); + } + + public function test_creating_book_page_does_not_use_template_if_not_visible() + { + $templatePage = $this->entities->templatePage(); + $templatePage->forceFill(['html' => '

My template page

', 'markdown' => '# My template page'])->save(); + $book = $this->bookUsingDefaultTemplate($templatePage); + $this->permissions->disableEntityInheritedPermissions($templatePage); + + $this->asEditor()->get($book->getUrl('/create-page')); + $latestPage = $book->pages() + ->where('draft', '=', true) + ->where('template', '=', false) + ->latest()->first(); + + $this->assertEquals('', $latestPage->html); + $this->assertEquals('', $latestPage->markdown); + } + + protected function bookUsingDefaultTemplate(Page $page): Book + { + $book = $this->entities->book(); + $book->default_template_id = $page->id; + $book->save(); + + return $book; + } +} diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php index 512f43fb6..cb036fe97 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -5,16 +5,21 @@ namespace Tests\Helpers; use BookStack\Entities\Models\Entity; use BookStack\Permissions\Models\EntityPermission; use BookStack\Permissions\Models\RolePermission; +use BookStack\Settings\SettingService; use BookStack\Users\Models\Role; use BookStack\Users\Models\User; class PermissionsProvider { - protected UserRoleProvider $userRoleProvider; + public function __construct( + protected UserRoleProvider $userRoleProvider + ) { + } - public function __construct(UserRoleProvider $userRoleProvider) + public function makeAppPublic(): void { - $this->userRoleProvider = $userRoleProvider; + $settings = app(SettingService::class); + $settings->put('app-public', 'true'); } /** From 2081a783f3795f300defea0ba2285fdc7d80f512 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 Dec 2023 15:38:09 +0000 Subject: [PATCH 10/12] Default templates: Cleaned up ux, added case for added endpoint Cleaned up and updated page picker a bit, allowing longer names to show, clicking through to item without triggering popup, and updated to use hidden attributes instead of styles. Added phpunit tests to cover supporting entity-selector-templates endpoint. --- resources/js/components/page-picker.js | 3 ++- resources/sass/_layout.scss | 6 +++++- resources/views/books/parts/form.blade.php | 17 ++++++++------- resources/views/form/page-picker.blade.php | 10 ++++----- resources/views/pages/edit.blade.php | 2 +- tests/Entity/EntitySearchTest.php | 25 ++++++++++++++++++++++ 6 files changed, 47 insertions(+), 16 deletions(-) diff --git a/resources/js/components/page-picker.js b/resources/js/components/page-picker.js index 130972fdd..9bb0bee04 100644 --- a/resources/js/components/page-picker.js +++ b/resources/js/components/page-picker.js @@ -1,7 +1,7 @@ import {Component} from './component'; function toggleElem(elem, show) { - elem.style.display = show ? null : 'none'; + elem.toggleAttribute('hidden', !show); } export class PagePicker extends Component { @@ -21,6 +21,7 @@ export class PagePicker extends Component { setupListeners() { this.selectButton.addEventListener('click', this.showPopup.bind(this)); this.display.parentElement.addEventListener('click', this.showPopup.bind(this)); + this.display.addEventListener('click', e => e.stopPropagation()); this.resetButton.addEventListener('click', () => { this.setValue('', ''); diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 94a36ecba..6c78419d8 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -270,10 +270,14 @@ body.flexbox { overflow: hidden; } -.fill-height { +.height-fill { height: 100%; } +.height-auto { + height: auto !important; +} + .float { float: left; &.right { diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index b4ca2fba5..973bae987 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -40,17 +40,18 @@
-
-

+

+

{{ trans('entities.books_default_template_explain') }}

- - @include('form.page-picker', [ - 'name' => 'default_template_id', - 'placeholder' => trans('entities.books_default_template_select'), - 'value' => $book?->default_template_id ?? null, - ]) +
+ @include('form.page-picker', [ + 'name' => 'default_template_id', + 'placeholder' => trans('entities.books_default_template_select'), + 'value' => $book?->default_template_id ?? null, + ]) +
diff --git a/resources/views/form/page-picker.blade.php b/resources/views/form/page-picker.blade.php index 90ce75676..d9810d575 100644 --- a/resources/views/form/page-picker.blade.php +++ b/resources/views/form/page-picker.blade.php @@ -1,13 +1,13 @@ {{--Depends on entity selector popup--}}
-
- - +
- - + +
\ No newline at end of file diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index 841b27503..58fb5f355 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -4,7 +4,7 @@ @section('content') -
+
{{ csrf_field() }} diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index fbb47226e..7841a255e 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -252,6 +252,31 @@ class EntitySearchTest extends TestCase $this->withHtml($resp)->assertElementContains($baseSelector, "You don't have the required permissions to select this item"); } + public function test_entity_template_selector_search() + { + $templatePage = $this->entities->newPage(['name' => 'Template search test', 'html' => 'template test']); + $templatePage->template = true; + $templatePage->save(); + + $nonTemplatePage = $this->entities->newPage(['name' => 'Nontemplate page', 'html' => 'nontemplate', 'template' => false]); + + // Visit both to make popular + $this->asEditor()->get($templatePage->getUrl()); + $this->asEditor()->get($nonTemplatePage->getUrl()); + + $normalSearch = $this->get('/search/entity-selector-templates?term=test'); + $normalSearch->assertSee($templatePage->name); + $normalSearch->assertDontSee($nonTemplatePage->name); + + $normalSearch = $this->get('/search/entity-selector-templates?term=beans'); + $normalSearch->assertDontSee($templatePage->name); + $normalSearch->assertDontSee($nonTemplatePage->name); + + $defaultListTest = $this->get('/search/entity-selector-templates'); + $defaultListTest->assertSee($templatePage->name); + $defaultListTest->assertDontSee($nonTemplatePage->name); + } + public function test_sibling_search_for_pages() { $chapter = $this->entities->chapterHasPages(); From 2f3806244cbbe589d3130efcef038b2a62f05afa Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 Dec 2023 15:41:56 +0000 Subject: [PATCH 11/12] Default templates: Added permission checks to selector test --- tests/Entity/EntitySearchTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 7841a255e..9b77a32ab 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -262,7 +262,7 @@ class EntitySearchTest extends TestCase // Visit both to make popular $this->asEditor()->get($templatePage->getUrl()); - $this->asEditor()->get($nonTemplatePage->getUrl()); + $this->get($nonTemplatePage->getUrl()); $normalSearch = $this->get('/search/entity-selector-templates?term=test'); $normalSearch->assertSee($templatePage->name); @@ -275,6 +275,14 @@ class EntitySearchTest extends TestCase $defaultListTest = $this->get('/search/entity-selector-templates'); $defaultListTest->assertSee($templatePage->name); $defaultListTest->assertDontSee($nonTemplatePage->name); + + $this->permissions->disableEntityInheritedPermissions($templatePage); + + $normalSearch = $this->get('/search/entity-selector-templates?term=test'); + $normalSearch->assertDontSee($templatePage->name); + + $defaultListTest = $this->get('/search/entity-selector-templates'); + $defaultListTest->assertDontSee($templatePage->name); } public function test_sibling_search_for_pages() From 3af07addf6742454f2f984dac24147e2fe65fde3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 Dec 2023 15:59:12 +0000 Subject: [PATCH 12/12] Default templates: Fixed syntax for php8.0, added test Null accessor is akward in php8.0 and throws warnings, so removed. Added test to check template assingment handling on page delete. --- resources/views/books/parts/form.blade.php | 2 +- tests/Entity/BookDefaultTemplateTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index 973bae987..e22be619d 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -49,7 +49,7 @@ @include('form.page-picker', [ 'name' => 'default_template_id', 'placeholder' => trans('entities.books_default_template_select'), - 'value' => $book?->default_template_id ?? null, + 'value' => $book->default_template_id ?? null, ])
diff --git a/tests/Entity/BookDefaultTemplateTest.php b/tests/Entity/BookDefaultTemplateTest.php index f23ba4a8e..d4cd5b2c3 100644 --- a/tests/Entity/BookDefaultTemplateTest.php +++ b/tests/Entity/BookDefaultTemplateTest.php @@ -159,6 +159,21 @@ class BookDefaultTemplateTest extends TestCase $this->assertEquals('', $latestPage->markdown); } + public function test_template_page_delete_removes_book_template_usage() + { + $templatePage = $this->entities->templatePage(); + $book = $this->bookUsingDefaultTemplate($templatePage); + + $book->refresh(); + $this->assertEquals($templatePage->id, $book->default_template_id); + + $this->asEditor()->delete($templatePage->getUrl()); + $this->asAdmin()->post('/settings/recycle-bin/empty'); + + $book->refresh(); + $this->assertEquals(null, $book->default_template_id); + } + protected function bookUsingDefaultTemplate(Page $page): Book { $book = $this->entities->book();