Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions app/Entities/Repos/PageRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use DOMDocument;
use DOMElement;
use DOMXPath;
use Illuminate\Support\Collection;

class PageRepo extends EntityRepo
{
Expand Down Expand Up @@ -69,6 +70,10 @@ public function updatePage(Page $page, int $book_id, array $input)
$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);
Expand All @@ -85,8 +90,9 @@ public function updatePage(Page $page, int $book_id, array $input)
$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);
Expand Down Expand Up @@ -300,6 +306,10 @@ public function publishPageDraft(Page $draftPage, array $input)
$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);
Expand Down Expand Up @@ -523,4 +533,29 @@ public function copyPage(Page $page, Entity $newParent, string $newName = '')

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;
}
}
10 changes: 8 additions & 2 deletions app/Http/Controllers/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,14 @@ public function editDraft($bookSlug, $pageId)
$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,
]);
}

Expand Down Expand Up @@ -239,11 +242,14 @@ public function edit($bookSlug, $pageSlug)
}

$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,
]);
}

Expand Down
63 changes: 63 additions & 0 deletions app/Http/Controllers/PageTemplateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace BookStack\Http\Controllers;

use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;

class PageTemplateController extends Controller
{
protected $pageRepo;

/**
* PageTemplateController constructor.
* @param $pageRepo
*/
public function __construct(PageRepo $pageRepo)
{
$this->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,
]);
}

}
54 changes: 54 additions & 0 deletions database/migrations/2019_07_07_112515_add_template_support.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

use Carbon\Carbon;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddTemplateSupport extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function (Blueprint $table) {
$table->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();
}
}
1 change: 1 addition & 0 deletions resources/assets/icons/chevron-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/assets/icons/template.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion resources/assets/js/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {};
Expand Down
32 changes: 32 additions & 0 deletions resources/assets/js/components/markdown-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class MarkdownEditor {
});

this.codeMirrorSetup();
this.listenForBookStackEditorEvents();
}

// Update the input content and render the display.
Expand Down Expand Up @@ -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 ;
85 changes: 85 additions & 0 deletions resources/assets/js/components/template-manager.js
Original file line number Diff line number Diff line change
@@ -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;
Loading