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 ef14f62210f..6f1a81d12ea 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 c209626cdbf..ee4467552e4 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 fc4556857c7..b84a351f841 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 @@ public function coverImageTypeKey(): string 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 14c3af1cc5c..9d8db27e93c 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -6,6 +6,7 @@ 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 @@ public function create(string $shelfSlug = null) $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 @@ public function store(Request $request, string $shelfSlug = null) 'description' => ['string', 'max:1000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'tags' => ['array'], + 'default_template' => ['nullable', 'exists:pages,id'], ]); $bookshelf = null; @@ -151,7 +159,12 @@ public function edit(string $slug) $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 @@ public function update(Request $request, string $slug) '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 00000000000..755f83b5c0b --- /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 fa2586f8d75..38c2f2ae391 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -328,6 +328,8 @@ '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 eead4191c34..6253a49bbb9 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 180500e0a04..9ec47293509 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 e893bceadc5..c6ef7d171f1 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 00000000000..fe04d9389ef --- /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 c8eddc398ca..a1558b85db8 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -148,6 +148,12 @@ public function getNewDraftPage(Entity $parent) $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 9e09aed16c1..394147ce224 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -74,7 +74,6 @@ public function createAsGuest(Request $request, string $bookSlug, string $chapte $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 394147ce224..8b131c4f3d8 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 @@ public function showDelete(string $bookSlug, string $pageSlug) $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 38c2f2ae391..4af6120f820 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -192,6 +192,7 @@ '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 39cd07bbb1c..9ce50d48b13 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 d6e1cae446a..11970b8b6e3 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 9b938d89a41..12df935b0a6 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -7,7 +7,6 @@ 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 @@ 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 @@ public function create(string $shelfSlug = null) $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 @@ public function store(Request $request, string $shelfSlug = null) { $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 @@ public function edit(string $slug) $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 @@ public function update(Request $request, string $slug) $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 ad75448b34a..11f19f72f11 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -259,13 +259,13 @@ public function showDelete(string $bookSlug, string $pageSlug) $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 8584e755e60..19aba052557 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -20,6 +20,7 @@ * @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 5269a0bccb6..9a183469b4a 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -136,9 +136,11 @@ public function getNewDraftPage(Entity $parent) $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 e4c67f5ca81..ee612b7ba4f 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -132,6 +132,8 @@ '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 @@ '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 @@ '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 6253a49bbb9..318abfcf137 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 9ec47293509..6efd1caea92 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 9b66b8ac87a..a6b0eade27b 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 fe04d9389ef..90c5e421b7e 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 9ce50d48b13..40125dfe2f4 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 11f19f72f11..d929341232a 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -279,11 +279,13 @@ public function showDeleteDraft(string $bookSlug, int $pageId) $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 19aba052557..faae276a5c0 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -15,6 +15,7 @@ * * @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 737caa70bb4..b46218fe0ee 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -5,6 +5,7 @@ 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 @@ 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 @@ public function update(Book $book, array $input): Book { $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 @@ public function update(Book $book, array $input): Book 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 08276230c40..b0c452456a0 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -202,6 +202,10 @@ protected function destroyPage(Page $page): int $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 09a67f2b5ca..6cf12a57920 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 @@ public function searchForSelector(Request $request) 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 d38fc8d5751..fffa03db094 100644 --- a/app/Search/SearchOptions.php +++ b/app/Search/SearchOptions.php @@ -170,6 +170,14 @@ protected static function parseStandardTermString(string $termString): array 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 fc36cb816c8..aac9d10005b 100644 --- a/app/Search/SearchRunner.php +++ b/app/Search/SearchRunner.php @@ -58,7 +58,7 @@ public function searchEntities(SearchOptions $searchOpts, string $entityType = ' $entityTypesToSearch = $entityTypes; if ($entityType !== 'all') { - $entityTypesToSearch = $entityType; + $entityTypesToSearch = [$entityType]; } elseif (isset($searchOpts->filters['type'])) { $entityTypesToSearch = explode('|', $searchOpts->filters['type']); } @@ -469,6 +469,13 @@ protected function filterNotViewedByMe(EloquentBuilder $query, Entity $model, $i }); } + 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 755f83b5c0b..913361dcbcf 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 ee612b7ba4f..354eee42e79 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -133,7 +133,8 @@ '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 @@ '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 9cda3587401..b12eeb402ba 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 d157ffdc36d..94a36ecba59 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 a6b0eade27b..b16468a09c4 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 90c5e421b7e..00000000000 --- 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 c896b50b523..d4c941e9a33 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 a9f5b932cc2..c1280cfb2f7 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 d599a19ab6a..90ce7567658 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 40125dfe2f4..a9c4b73ad74 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 be99cc25476..7112ebcff64 100644 --- a/resources/views/settings/customization.blade.php +++ b/resources/views/settings/customization.blade.php @@ -133,7 +133,7 @@ class="p-m">
diff --git a/routes/web.php b/routes/web.php index 8fc90ee54c4..4620cd08bc3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -182,6 +182,7 @@ 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 cb67184a085..41ff11ddec3 100644 --- a/app/Entities/Controllers/BookApiController.php +++ b/app/Entities/Controllers/BookApiController.php @@ -14,11 +14,9 @@ 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 @@ public function create(Request $request) */ 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 @@ protected function rules(): array '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 12df935b0a6..faa5788938e 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -92,11 +92,11 @@ public function store(Request $request, string $shelfSlug = null) { $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 @@ public function update(Request $request, string $slug) $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 d929341232a..0a3e76daa42 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -259,7 +259,7 @@ public function showDelete(string $bookSlug, string $pageSlug) $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 @@ public function showDeleteDraft(string $bookSlug, int $pageId) $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 faae276a5c0..ee9a7f44722 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -15,7 +15,7 @@ * * @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 @@ public function coverImageTypeKey(): string */ 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 b46218fe0ee..03e1118b122 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -86,6 +86,7 @@ public function create(array $input): Book $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 @@ public function update(Book $book, array $input): Book { $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 @@ public function update(Book $book, array $input): Book */ 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 @@ protected function updateBookDefaultTemplate(Book $book, int $templateId): void ->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 b0c452456a0..b2510398541 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -203,8 +203,8 @@ protected function destroyPage(Page $page): int } // 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 913361dcbcf..c23bebc2e63 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 @@ public function up() 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 4a66266196b..2a38dba8392 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 fc67d5fccdd..c026b7b4943 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 12a3e9e9fcd..77387912535 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 3744445d050..21e1829b8eb 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 7d3d6735e1e..f69677c4ad7 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 b16468a09c4..b4ca2fba5ea 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 326304d6f11..c648faaf2f4 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -31,13 +31,16 @@ public function test_index_endpoint_returns_expected_book() 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 @@ public function test_read_endpoint() 'owned_by' => [ 'name' => $book->ownedBy->name, ], + 'default_template_id' => null, ]); } @@ -121,9 +125,11 @@ public function test_update_endpoint() { $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 3cb8c44d32c..98206342145 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -53,6 +53,15 @@ public function pageNotWithinChapter(): Page 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 00000000000..f23ba4a8e2d --- /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 512f43fb6cc..cb036fe97c7 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -5,16 +5,21 @@ 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 130972fdd63..9bb0bee04a3 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 94a36ecba59..6c78419d86c 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 b4ca2fba5ea..973bae98701 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 90ce7567658..d9810d575df 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 841b275037b..58fb5f355ea 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 fbb47226e6a..7841a255e0e 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -252,6 +252,31 @@ public function test_entity_selector_search_reflects_items_without_permission() $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 7841a255e0e..9b77a32ab80 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -262,7 +262,7 @@ public function test_entity_template_selector_search() // 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 @@ public function test_entity_template_selector_search() $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 973bae98701..e22be619d66 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 f23ba4a8e2d..d4cd5b2c3a9 100644 --- a/tests/Entity/BookDefaultTemplateTest.php +++ b/tests/Entity/BookDefaultTemplateTest.php @@ -159,6 +159,21 @@ public function test_creating_book_page_does_not_use_template_if_not_visible() $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();