From 7ebe7d4e58f4555d6a9a253f976e22af9add7dec Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 11 Dec 2023 15:55:43 +0000 Subject: [PATCH] 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 (86%) 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 86% 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