diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 6b004984f..ed142eb61 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -9,6 +9,7 @@ use Carbon\Carbon; use DOMDocument; use DOMElement; use DOMXPath; +use Illuminate\Support\Collection; class PageRepo extends EntityRepo { @@ -69,6 +70,10 @@ class PageRepo extends EntityRepo $this->tagRepo->saveTagsToEntity($page, $input['tags']); } + if (isset($input['template']) && userCan('templates-manage')) { + $page->template = ($input['template'] === 'true'); + } + // Update with new details $userId = user()->id; $page->fill($input); @@ -85,8 +90,9 @@ class PageRepo extends EntityRepo $this->userUpdatePageDraftsQuery($page, $userId)->delete(); // Save a revision after updating - if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { - $this->savePageRevision($page, $input['summary']); + $summary = $input['summary'] ?? null; + if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) { + $this->savePageRevision($page, $summary); } $this->searchService->indexEntity($page); @@ -300,6 +306,10 @@ class PageRepo extends EntityRepo $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']); } + if (isset($input['template']) && userCan('templates-manage')) { + $draftPage->template = ($input['template'] === 'true'); + } + $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id); $draftPage->html = $this->formatHtml($input['html']); $draftPage->text = $this->pageToPlainText($draftPage); @@ -523,4 +533,29 @@ class PageRepo extends EntityRepo return $this->publishPageDraft($copyPage, $pageData); } + + /** + * Get pages that have been marked as templates. + * @param int $count + * @param int $page + * @param string $search + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getPageTemplates(int $count = 10, int $page = 1, string $search = '') + { + $query = $this->entityQuery('page') + ->where('template', '=', true) + ->orderBy('name', 'asc') + ->skip( ($page - 1) * $count) + ->take($count); + + if ($search) { + $query->where('name', 'like', '%' . $search . '%'); + } + + $paginator = $query->paginate($count, ['*'], 'page', $page); + $paginator->withPath('/templates'); + + return $paginator; + } } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 89fb83fd9..8819510a6 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -110,11 +110,14 @@ class PageController extends Controller $this->setPageTitle(trans('entities.pages_edit_draft')); $draftsEnabled = $this->signedIn; + $templates = $this->pageRepo->getPageTemplates(10); + return view('pages.edit', [ 'page' => $draft, 'book' => $draft->book, 'isDraft' => true, - 'draftsEnabled' => $draftsEnabled + 'draftsEnabled' => $draftsEnabled, + 'templates' => $templates, ]); } @@ -239,11 +242,14 @@ class PageController extends Controller } $draftsEnabled = $this->signedIn; + $templates = $this->pageRepo->getPageTemplates(10); + return view('pages.edit', [ 'page' => $page, 'book' => $page->book, 'current' => $page, - 'draftsEnabled' => $draftsEnabled + 'draftsEnabled' => $draftsEnabled, + 'templates' => $templates, ]); } diff --git a/app/Http/Controllers/PageTemplateController.php b/app/Http/Controllers/PageTemplateController.php new file mode 100644 index 000000000..05943351a --- /dev/null +++ b/app/Http/Controllers/PageTemplateController.php @@ -0,0 +1,63 @@ +pageRepo = $pageRepo; + parent::__construct(); + } + + /** + * Fetch a list of templates from the system. + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function list(Request $request) + { + $page = $request->get('page', 1); + $search = $request->get('search', ''); + $templates = $this->pageRepo->getPageTemplates(10, $page, $search); + + if ($search) { + $templates->appends(['search' => $search]); + } + + return view('pages.template-manager-list', [ + 'templates' => $templates + ]); + } + + /** + * Get the content of a template. + * @param $templateId + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response + * @throws NotFoundException + */ + public function get($templateId) + { + $page = $this->pageRepo->getById('page', $templateId); + + if (!$page->template) { + throw new NotFoundException(); + } + + return response()->json([ + 'html' => $page->html, + 'markdown' => $page->markdown, + ]); + } + +} diff --git a/database/migrations/2019_07_07_112515_add_template_support.php b/database/migrations/2019_07_07_112515_add_template_support.php new file mode 100644 index 000000000..a54508198 --- /dev/null +++ b/database/migrations/2019_07_07_112515_add_template_support.php @@ -0,0 +1,54 @@ +boolean('template')->default(false); + $table->index('template'); + }); + + // Create new templates-manage permission and assign to admin role + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => 'templates-manage', + 'display_name' => 'Manage Page Templates', + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('pages', function (Blueprint $table) { + $table->dropColumn('template'); + }); + + // Remove templates-manage permission + $templatesManagePermission = DB::table('role_permissions') + ->where('name', '=', 'templates_manage')->first(); + + DB::table('permission_role')->where('permission_id', '=', $templatesManagePermission->id)->delete(); + DB::table('role_permissions')->where('name', '=', 'templates_manage')->delete(); + } +} diff --git a/readme.md b/readme.md index 276a3de20..62e2aa65d 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,7 @@ [![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE) [![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack) +[![Discord](https://img.shields.io/static/v1?label=Chat&message=Discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2) A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. diff --git a/resources/assets/icons/chevron-down.svg b/resources/assets/icons/chevron-down.svg new file mode 100644 index 000000000..f08dfafcb --- /dev/null +++ b/resources/assets/icons/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/assets/icons/template.svg b/resources/assets/icons/template.svg new file mode 100644 index 000000000..7c142124f --- /dev/null +++ b/resources/assets/icons/template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index 1c2abd520..8c12da9b4 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -27,6 +27,7 @@ import customCheckbox from "./custom-checkbox"; import bookSort from "./book-sort"; import settingAppColorPicker from "./setting-app-color-picker"; import entityPermissionsEditor from "./entity-permissions-editor"; +import templateManager from "./template-manager"; const componentMapping = { 'dropdown': dropdown, @@ -57,7 +58,8 @@ const componentMapping = { 'custom-checkbox': customCheckbox, 'book-sort': bookSort, 'setting-app-color-picker': settingAppColorPicker, - 'entity-permissions-editor': entityPermissionsEditor + 'entity-permissions-editor': entityPermissionsEditor, + 'template-manager': templateManager, }; window.components = {}; diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index b0e4d693a..7cb56eef8 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -91,6 +91,7 @@ class MarkdownEditor { }); this.codeMirrorSetup(); + this.listenForBookStackEditorEvents(); } // Update the input content and render the display. @@ -461,6 +462,37 @@ class MarkdownEditor { }) } + listenForBookStackEditorEvents() { + + function getContentToInsert({html, markdown}) { + return markdown || html; + } + + // Replace editor content + window.$events.listen('editor::replace', (eventContent) => { + const markdown = getContentToInsert(eventContent); + this.cm.setValue(markdown); + }); + + // Append editor content + window.$events.listen('editor::append', (eventContent) => { + const cursorPos = this.cm.getCursor('from'); + const markdown = getContentToInsert(eventContent); + const content = this.cm.getValue() + '\n' + markdown; + this.cm.setValue(content); + this.cm.setCursor(cursorPos.line, cursorPos.ch); + }); + + // Prepend editor content + window.$events.listen('editor::prepend', (eventContent) => { + const cursorPos = this.cm.getCursor('from'); + const markdown = getContentToInsert(eventContent); + const content = markdown + '\n' + this.cm.getValue(); + this.cm.setValue(content); + const prependLineCount = markdown.split('\n').length; + this.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch); + }); + } } export default MarkdownEditor ; diff --git a/resources/assets/js/components/template-manager.js b/resources/assets/js/components/template-manager.js new file mode 100644 index 000000000..b966762d2 --- /dev/null +++ b/resources/assets/js/components/template-manager.js @@ -0,0 +1,85 @@ +import * as DOM from "../services/dom"; + +class TemplateManager { + + constructor(elem) { + this.elem = elem; + this.list = elem.querySelector('[template-manager-list]'); + this.searching = false; + + // Template insert action buttons + DOM.onChildEvent(this.elem, '[template-action]', 'click', this.handleTemplateActionClick.bind(this)); + + // Template list pagination click + DOM.onChildEvent(this.elem, '.pagination a', 'click', this.handlePaginationClick.bind(this)); + + // Template list item content click + DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this)); + + this.setupSearchBox(); + } + + handleTemplateItemClick(event, templateItem) { + const templateId = templateItem.closest('[template-id]').getAttribute('template-id'); + this.insertTemplate(templateId, 'replace'); + } + + handleTemplateActionClick(event, actionButton) { + event.stopPropagation(); + + const action = actionButton.getAttribute('template-action'); + const templateId = actionButton.closest('[template-id]').getAttribute('template-id'); + this.insertTemplate(templateId, action); + } + + async insertTemplate(templateId, action = 'replace') { + const resp = await window.$http.get(`/templates/${templateId}`); + const eventName = 'editor::' + action; + window.$events.emit(eventName, resp.data); + } + + async handlePaginationClick(event, paginationLink) { + event.preventDefault(); + const paginationUrl = paginationLink.getAttribute('href'); + const resp = await window.$http.get(paginationUrl); + this.list.innerHTML = resp.data; + } + + setupSearchBox() { + const searchBox = this.elem.querySelector('.search-box'); + const input = searchBox.querySelector('input'); + const submitButton = searchBox.querySelector('button'); + const cancelButton = searchBox.querySelector('button.search-box-cancel'); + + async function performSearch() { + const searchTerm = input.value; + const resp = await window.$http.get(`/templates`, { + search: searchTerm + }); + cancelButton.style.display = searchTerm ? 'block' : 'none'; + this.list.innerHTML = resp.data; + } + performSearch = performSearch.bind(this); + + // Searchbox enter press + searchBox.addEventListener('keypress', event => { + if (event.key === 'Enter') { + event.preventDefault(); + performSearch(); + } + }); + + // Submit button press + submitButton.addEventListener('click', event => { + performSearch(); + }); + + // Cancel button press + cancelButton.addEventListener('click', event => { + input.value = ''; + performSearch(); + }); + } +} + +export default TemplateManager; \ No newline at end of file diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js index eb9f025a7..be0aaf18a 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -378,6 +378,27 @@ function customHrPlugin() { } +function listenForBookStackEditorEvents(editor) { + + // Replace editor content + window.$events.listen('editor::replace', ({html}) => { + editor.setContent(html); + }); + + // Append editor content + window.$events.listen('editor::append', ({html}) => { + const content = editor.getContent() + html; + editor.setContent(content); + }); + + // Prepend editor content + window.$events.listen('editor::prepend', ({html}) => { + const content = html + editor.getContent(); + editor.setContent(content); + }); + +} + class WysiwygEditor { constructor(elem) { @@ -553,6 +574,10 @@ class WysiwygEditor { editor.focus(); } + listenForBookStackEditorEvents(editor); + + // TODO - Update to standardise across both editors + // Use events within listenForBookStackEditorEvents instead (Different event signature) window.$events.listen('editor-html-update', html => { editor.setContent(html); editor.selection.select(editor.getBody(), true); diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss index 032b1cbeb..5f11c2355 100644 --- a/resources/assets/sass/_blocks.scss +++ b/resources/assets/sass/_blocks.scss @@ -83,6 +83,10 @@ line-height: 1; } +.card.border-card { + border: 1px solid #DDD; +} + .card.drag-card { border: 1px solid #DDD; border-radius: 4px; diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 039ac4dc8..0b683c6e3 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -655,4 +655,32 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } .permissions-table tr:hover [permissions-table-toggle-all-in-row] { display: inline; +} + +.template-item { + cursor: pointer; + position: relative; + &:hover, .template-item-actions button:hover { + background-color: #F2F2F2; + } + .template-item-actions { + position: absolute; + top: 0; + right: 0; + width: 50px; + height: 100%; + display: flex; + flex-direction: column; + border-left: 1px solid #DDD; + } + .template-item-actions button { + cursor: pointer; + flex: 1; + background: #FFF; + border: 0; + border-top: 1px solid #DDD; + } + .template-item-actions button:first-child { + border-top: 0; + } } \ No newline at end of file diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index fc784eebe..be0cc381c 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -262,7 +262,7 @@ body.mce-fullscreen .page-editor .edit-area { display: block; cursor: pointer; padding: $-s $-m; - font-size: 13.5px; + font-size: 16px; line-height: 1.6; border-bottom: 1px solid rgba(255, 255, 255, 0.3); } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index f6df7e71b..3208a6dfc 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -233,6 +233,7 @@ return [ ], 'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content', 'pages_specific' => 'Specific Page', + 'pages_is_template' => 'Page Template', // Editor Sidebar 'page_tags' => 'Page Tags', @@ -269,6 +270,12 @@ return [ 'attachments_file_uploaded' => 'File successfully uploaded', 'attachments_file_updated' => 'File successfully updated', 'attachments_link_attached' => 'Link successfully attached to page', + 'templates' => 'Templates', + 'templates_set_as_template' => 'Page is a template', + 'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.', + 'templates_replace_content' => 'Replace page content', + 'templates_append_content' => 'Append to page content', + 'templates_prepend_content' => 'Prepend to page content', // Profile View 'profile_user_for_x' => 'User for :time', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index d275e330a..78f86b68e 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -85,6 +85,7 @@ return [ 'role_manage_roles' => 'Manage roles & role permissions', 'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions', 'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages', + 'role_manage_page_templates' => 'Manage page templates', 'role_manage_settings' => 'Manage app settings', 'role_asset' => 'Asset Permissions', 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', diff --git a/resources/views/pages/attachment-manager.blade.php b/resources/views/pages/attachment-manager.blade.php new file mode 100644 index 000000000..7b16c6b7b --- /dev/null +++ b/resources/views/pages/attachment-manager.blade.php @@ -0,0 +1,99 @@ +
+ + @exposeTranslations([ + 'entities.attachments_file_uploaded', + 'entities.attachments_file_updated', + 'entities.attachments_link_attached', + 'entities.attachments_updated_success', + 'errors.server_upload_limit', + 'components.image_upload_remove', + 'components.file_upload_timeout', + ]) + +

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

+
+ +
+

{{ trans('entities.attachments_explain') }} {{ trans('entities.attachments_explain_instant_save') }}

+ +
+ +
+ +
+
@icon('grip')
+
+ +
+ {{ trans('entities.attachments_delete_confirm') }} +
+ {{ trans('common.cancel') }} +
+
+
@icon('edit')
+
@icon('close')
+
+
+

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

+
+
+ +
+
+

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

+
+ + +

+
+
+ + +

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

+
+ +
+ +
+ +
+
+
+
+ + +

+
+
+
+ + + +
+ +
+
\ No newline at end of file diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index 4930e30a3..cfb66fdd0 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -16,7 +16,7 @@ @endif @include('pages.form', ['model' => $page]) - @include('pages.form-toolbox') + @include('pages.editor-toolbox') diff --git a/resources/views/pages/editor-toolbox.blade.php b/resources/views/pages/editor-toolbox.blade.php new file mode 100644 index 000000000..3ce4cfbf3 --- /dev/null +++ b/resources/views/pages/editor-toolbox.blade.php @@ -0,0 +1,32 @@ +
+ +
+ @icon('caret-left-circle') + @icon('tag') + @if(userCan('attachment-create-all')) + @icon('attach') + @endif + @icon('template') +
+ +
+

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

+
+ @include('components.tag-manager', ['entity' => $page, 'entityType' => 'page']) +
+
+ + @if(userCan('attachment-create-all')) + @include('pages.attachment-manager', ['page' => $page]) + @endif + +
+

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

+ +
+ @include('pages.template-manager', ['page' => $page, 'templates' => $templates]) +
+ +
+ +
diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php deleted file mode 100644 index d69be20c1..000000000 --- a/resources/views/pages/form-toolbox.blade.php +++ /dev/null @@ -1,121 +0,0 @@ - -
- -
- @icon('caret-left-circle') - @icon('tag') - @if(userCan('attachment-create-all')) - @icon('attach') - @endif -
- -
-

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

-
- @include('components.tag-manager', ['entity' => $page, 'entityType' => 'page']) -
-
- - @if(userCan('attachment-create-all')) -
- - @exposeTranslations([ - 'entities.attachments_file_uploaded', - 'entities.attachments_file_updated', - 'entities.attachments_link_attached', - 'entities.attachments_updated_success', - 'errors.server_upload_limit', - 'components.image_upload_remove', - 'components.file_upload_timeout', - ]) - -

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

-
- -
-

{{ trans('entities.attachments_explain') }} {{ trans('entities.attachments_explain_instant_save') }}

- -
- -
- -
-
@icon('grip')
-
- -
- {{ trans('entities.attachments_delete_confirm') }} -
- {{ trans('common.cancel') }} -
-
-
@icon('edit')
-
@icon('close')
-
-
-

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

-
-
- -
-
-

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

-
- - -

-
-
- - -

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

-
- -
- -
- -
-
-
-
- - -

-
-
-
- - - -
- -
-
- @endif - -
diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index fb0df2ddd..86b0d3f88 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -103,6 +103,12 @@ @endif @endif + + @if($page->template) +
+ @icon('template'){{ trans('entities.pages_is_template') }} +
+ @endif diff --git a/resources/views/pages/template-manager-list.blade.php b/resources/views/pages/template-manager-list.blade.php new file mode 100644 index 000000000..68899c8a5 --- /dev/null +++ b/resources/views/pages/template-manager-list.blade.php @@ -0,0 +1,20 @@ +{{ $templates->links() }} + +@foreach($templates as $template) +
+
+
{{ $template->name }}
+
{{ trans('entities.meta_updated', ['timeLength' => $template->updated_at->diffForHumans()]) }}
+
+
+ + +
+
+@endforeach + +{{ $templates->links() }} \ No newline at end of file diff --git a/resources/views/pages/template-manager.blade.php b/resources/views/pages/template-manager.blade.php new file mode 100644 index 000000000..fbdb70a1b --- /dev/null +++ b/resources/views/pages/template-manager.blade.php @@ -0,0 +1,25 @@ +
+ @if(userCan('templates-manage')) +

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

+ @include('components.toggle-switch', [ + 'name' => 'template', + 'value' => old('template', $page->template ? 'true' : 'false') === 'true', + 'label' => trans('entities.templates_set_as_template') + ]) +
+ @endif + + @if(count($templates) > 0) + + @endif + +
+ @include('pages.template-manager-list', ['templates' => $templates]) +
+
\ No newline at end of file diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 68b841e03..a9933a7a6 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -38,6 +38,7 @@
@include('settings.roles.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])
@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])
@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])
+
@include('settings.roles.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])
@include('settings.roles.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])
diff --git a/routes/web.php b/routes/web.php index 94dd576fe..11ca5d1af 100644 --- a/routes/web.php +++ b/routes/web.php @@ -158,6 +158,9 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter'); Route::get('/search/entity/siblings', 'SearchController@searchSiblings'); + Route::get('/templates', 'PageTemplateController@list'); + Route::get('/templates/{templateId}', 'PageTemplateController@get'); + // Other Pages Route::get('/', 'HomeController@index'); Route::get('/home', 'HomeController@index'); diff --git a/tests/Entity/PageTemplateTest.php b/tests/Entity/PageTemplateTest.php new file mode 100644 index 000000000..883de4a9f --- /dev/null +++ b/tests/Entity/PageTemplateTest.php @@ -0,0 +1,90 @@ +asEditor(); + $templateView = $this->get($page->getUrl()); + $templateView->assertDontSee('Page Template'); + + $page->template = true; + $page->save(); + + $templateView = $this->get($page->getUrl()); + $templateView->assertSee('Page Template'); + } + + public function test_manage_templates_permission_required_to_change_page_template_status() + { + $page = Page::first(); + $editor = $this->getEditor(); + $this->actingAs($editor); + + $pageUpdateData = [ + 'name' => $page->name, + 'html' => $page->html, + 'template' => 'true', + ]; + + $this->put($page->getUrl(), $pageUpdateData); + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, + 'template' => false, + ]); + + $this->giveUserPermissions($editor, ['templates-manage']); + + $this->put($page->getUrl(), $pageUpdateData); + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, + 'template' => true, + ]); + } + + public function test_templates_content_should_be_fetchable_only_if_page_marked_as_template() + { + $content = '
my_custom_template_content
'; + $page = Page::first(); + $editor = $this->getEditor(); + $this->actingAs($editor); + + $templateFetch = $this->get('/templates/' . $page->id); + $templateFetch->assertStatus(404); + + $page->html = $content; + $page->template = true; + $page->save(); + + $templateFetch = $this->get('/templates/' . $page->id); + $templateFetch->assertStatus(200); + $templateFetch->assertJson([ + 'html' => $content, + 'markdown' => '', + ]); + } + + public function test_template_endpoint_returns_paginated_list_of_templates() + { + $editor = $this->getEditor(); + $this->actingAs($editor); + + $toBeTemplates = Page::query()->orderBy('name', 'asc')->take(12)->get(); + $page = $toBeTemplates->first(); + + $emptyTemplatesFetch = $this->get('/templates'); + $emptyTemplatesFetch->assertDontSee($page->name); + + Page::query()->whereIn('id', $toBeTemplates->pluck('id')->toArray())->update(['template' => true]); + + $templatesFetch = $this->get('/templates'); + $templatesFetch->assertSee($page->name); + $templatesFetch->assertSee('pagination'); + } + +} \ No newline at end of file