Closes #80.
public function index()
{
$activity = Activity::latest(10);
- $recents = $this->signedIn ? Views::getUserRecentlyViewed(12, 0) : $this->entityRepo->getRecentlyCreatedBooks(10);
+ $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
+ $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
+ $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
return view('home', [
'activity' => $activity,
'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
- 'recentlyUpdatedPages' => $recentlyUpdatedPages
+ 'recentlyUpdatedPages' => $recentlyUpdatedPages,
+ 'draftPages' => $draftPages
]);
}
parent::__construct();
}
-
/**
* Get all images for a specific type, Paginated
* @param int $page
return response()->json($imgData);
}
-
/**
* Handles image uploads for use on pages.
* @param string $type
return response()->json($image);
}
-
/**
* Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo
public function create($bookSlug, $chapterSlug = false)
{
$book = $this->bookRepo->getBySlug($bookSlug);
- $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
+ $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$this->setPageTitle('Create New Page');
- return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
+
+ $draft = $this->pageRepo->getDraftPage($book, $chapter);
+ return redirect($draft->getUrl());
+ }
+
+ /**
+ * Show form to continue editing a draft page.
+ * @param $bookSlug
+ * @param $pageId
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function editDraft($bookSlug, $pageId)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $draft = $this->pageRepo->getById($pageId, true);
+ $this->checkOwnablePermission('page-create', $draft);
+ $this->setPageTitle('Edit Page Draft');
+
+ return view('pages/create', ['draft' => $draft, 'book' => $book]);
}
/**
- * Store a newly created page in storage.
+ * Store a new page by changing a draft into a page.
* @param Request $request
- * @param $bookSlug
+ * @param string $bookSlug
* @return Response
*/
- public function store(Request $request, $bookSlug)
+ public function store(Request $request, $bookSlug, $pageId)
{
$this->validate($request, [
- 'name' => 'required|string|max:255'
+ 'name' => 'required|string|max:255'
]);
$input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug);
- $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
- $parent = $chapterId !== null ? $this->chapterRepo->getById($chapterId) : $book;
- $this->checkOwnablePermission('page-create', $parent);
$input['priority'] = $this->bookRepo->getNewPriority($book);
- $page = $this->pageRepo->saveNew($input, $book, $chapterId);
+ $draftPage = $this->pageRepo->getById($pageId, true);
+
+ $chapterId = $draftPage->chapter_id;
+ $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
+ $this->checkOwnablePermission('page-create', $parent);
+
+ $page = $this->pageRepo->publishDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
$this->setPageTitle('Editing Page ' . $page->getShortName());
$page->isDraft = false;
- // Check for active editing and drafts
+ // Check for active editing
$warnings = [];
if ($this->pageRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
}
+ // Check for a current draft version for this user
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
$page->name = $draft->name;
public function update(Request $request, $bookSlug, $pageSlug)
{
$this->validate($request, [
- 'name' => 'required|string|max:255'
+ 'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
* @param $pageId
* @return \Illuminate\Http\JsonResponse
*/
- public function saveUpdateDraft(Request $request, $pageId)
+ public function saveDraft(Request $request, $pageId)
{
- $this->validate($request, [
- 'name' => 'required|string|max:255'
- ]);
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page);
- $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html']));
+ if ($page->draft) {
+ $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html']));
+ } else {
+ $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html']));
+ }
$updateTime = $draft->updated_at->format('H:i');
return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
}
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
}
+
+ /**
+ * Show the deletion page for the specified page.
+ * @param $bookSlug
+ * @param $pageId
+ * @return \Illuminate\View\View
+ * @throws NotFoundException
+ */
+ public function showDeleteDraft($bookSlug, $pageId)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $page = $this->pageRepo->getById($pageId, true);
+ $this->checkOwnablePermission('page-update', $page);
+ $this->setPageTitle('Delete Draft Page ' . $page->getShortName());
+ return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
+ }
+
/**
* Remove the specified page from storage.
- *
* @param $bookSlug
* @param $pageSlug
* @return Response
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name);
+ session()->flash('success', 'Page deleted');
+ $this->pageRepo->destroy($page);
+ return redirect($book->getUrl());
+ }
+
+ /**
+ * Remove the specified draft page from storage.
+ * @param $bookSlug
+ * @param $pageId
+ * @return Response
+ * @throws NotFoundException
+ */
+ public function destroyDraft($bookSlug, $pageId)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $page = $this->pageRepo->getById($pageId, true);
+ $this->checkOwnablePermission('page-update', $page);
+ session()->flash('success', 'Draft deleted');
$this->pageRepo->destroy($page);
return redirect($book->getUrl());
}
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
- 'Content-Type' => 'application/octet-stream',
- 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
}
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
- 'Content-Type' => 'application/octet-stream',
- 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
}
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
- 'Content-Type' => 'application/octet-stream',
- 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
}
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
- 'page' => $page,
+ 'page' => $page,
'roles' => $roles
]);
}
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
- Route::post('/{bookSlug}/page', 'PageController@store');
+ Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
+ Route::post('/{bookSlug}/page/{pageId}', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
+ Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict');
Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
+ Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
// Revisions
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
});
// Ajax routes
- Route::put('/ajax/page/{id}/save-draft', 'PageController@saveUpdateDraft');
+ Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
+ Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
// Links
Route::get('/link/{id}', 'PageController@redirectFromLink');
public function getUrl()
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
- return '/books/' . $bookSlug . '/page/' . $this->slug;
+ $midText = $this->draft ? '/draft/' : '/page/';
+ $idComponent = $this->draft ? $this->id : $this->slug;
+ return '/books/' . $bookSlug . $midText . $idComponent;
}
public function getExcerpt($length = 100)
$chapters = $chapterQuery->get();
$children = $pages->merge($chapters);
$bookSlug = $book->slug;
+
$children->each(function ($child) use ($bookSlug) {
$child->setAttribute('bookSlug', $bookSlug);
if ($child->isA('chapter')) {
$child->pages->each(function ($page) use ($bookSlug) {
$page->setAttribute('bookSlug', $bookSlug);
});
+ $child->pages = $child->pages->sortBy(function($child, $key) {
+ $score = $child->priority;
+ if ($child->draft) $score -= 100;
+ return $score;
+ });
}
});
- return $children->sortBy('priority');
+
+ // Sort items with drafts first then by priority.
+ return $children->sortBy(function($child, $key) {
+ $score = $child->priority;
+ if ($child->isA('page') && $child->draft) $score -= 100;
+ return $score;
+ });
}
/**
*/
public function getChildren(Chapter $chapter)
{
- return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
+ $pages = $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
+ // Sort items with drafts first then by priority.
+ return $pages->sortBy(function($child, $key) {
+ $score = $child->priority;
+ if ($child->draft) $score -= 100;
+ return $score;
+ });
}
/**
use BookStack\Entity;
use BookStack\Page;
use BookStack\Services\RestrictionService;
+use BookStack\User;
class EntityRepo
{
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->restrictionService->enforcePageRestrictions($this->page)
- ->orderBy('created_at', 'desc');
+ ->orderBy('created_at', 'desc')->where('draft', '=', false);
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
public function getRecentlyUpdatedPages($count = 20, $page = 0)
{
return $this->restrictionService->enforcePageRestrictions($this->page)
+ ->where('draft', '=', false)
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
}
+ /**
+ * Get draft pages owned by the current user.
+ * @param int $count
+ * @param int $page
+ */
+ public function getUserDraftPages($count = 20, $page = 0)
+ {
+ $user = auth()->user();
+ return $this->page->where('draft', '=', true)
+ ->where('created_by', '=', $user->id)
+ ->orderBy('updated_at', 'desc')
+ ->skip($count * $page)->take($count)->get();
+ }
+
/**
* Updates entity restrictions from a request
* @param $request
<?php namespace BookStack\Repos;
-
use Activity;
use BookStack\Book;
+use BookStack\Chapter;
use BookStack\Exceptions\NotFoundException;
use Carbon\Carbon;
use DOMDocument;
class PageRepo extends EntityRepo
{
+
protected $pageRevision;
/**
/**
* Base query for getting pages, Takes restrictions into account.
+ * @param bool $allowDrafts
* @return mixed
*/
- private function pageQuery()
+ private function pageQuery($allowDrafts = false)
{
- return $this->restrictionService->enforcePageRestrictions($this->page, 'view');
+ $query = $this->restrictionService->enforcePageRestrictions($this->page, 'view');
+ if (!$allowDrafts) {
+ $query = $query->where('draft', '=', false);
+ }
+ return $query;
}
/**
* Get a page via a specific ID.
* @param $id
+ * @param bool $allowDrafts
* @return mixed
*/
- public function getById($id)
+ public function getById($id, $allowDrafts = false)
{
- return $this->pageQuery()->findOrFail($id);
+ return $this->pageQuery($allowDrafts)->findOrFail($id);
}
/**
return $page;
}
+
+ /**
+ * Publish a draft page to make it a normal page.
+ * Sets the slug and updates the content.
+ * @param Page $draftPage
+ * @param array $input
+ * @return Page
+ */
+ public function publishDraft(Page $draftPage, array $input)
+ {
+ $draftPage->fill($input);
+
+ $draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
+ $draftPage->html = $this->formatHtml($input['html']);
+ $draftPage->text = strip_tags($draftPage->html);
+ $draftPage->draft = false;
+
+ $draftPage->save();
+ return $draftPage;
+ }
+
+ /**
+ * Get a new draft page instance.
+ * @param Book $book
+ * @param Chapter|null $chapter
+ * @return static
+ */
+ public function getDraftPage(Book $book, $chapter)
+ {
+ $page = $this->page->newInstance();
+ $page->name = 'New Page';
+ $page->created_by = auth()->user()->id;
+ $page->updated_by = auth()->user()->id;
+ $page->draft = true;
+
+ if ($chapter) $page->chapter_id = $chapter->id;
+
+ $book->pages()->save($page);
+ return $page;
+ }
+
/**
* Formats a page's html to be tagged correctly
* within the system.
return $draft;
}
+ /**
+ * Update a draft page.
+ * @param Page $page
+ * @param array $data
+ * @return Page
+ */
+ public function updateDraftPage(Page $page, $data = [])
+ {
+ $page->fill($data);
+
+ if (isset($data['html'])) {
+ $page->text = strip_tags($data['html']);
+ }
+
+ $page->save();
+ return $page;
+ }
+
/**
* The base query for getting user update drafts.
* @param Page $page
protected $userRoles;
protected $isAdmin;
protected $currentAction;
+ protected $currentUser;
/**
* RestrictionService constructor.
*/
public function __construct()
{
- $user = auth()->user();
- $this->userRoles = $user ? auth()->user()->roles->pluck('id') : [];
- $this->isAdmin = $user ? auth()->user()->hasRole('admin') : false;
+ $this->currentUser = auth()->user();
+ $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
+ $this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
}
/**
*/
public function enforcePageRestrictions($query, $action = 'view')
{
+ // Prevent drafts being visible to others.
+ $query = $query->where(function($query) {
+ $query->where('draft', '=', false);
+ if ($this->currentUser) {
+ $query->orWhere(function($query) {
+ $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
+ });
+ }
+ });
+
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->pageRestrictionQuery($query);
--- /dev/null
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class ImageEntitiesAndPageDrafts extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('images', function (Blueprint $table) {
+ $table->string('entity_type', 100);
+ $table->integer('entity_id');
+ $table->index(['entity_type', 'entity_id']);
+ });
+
+ Schema::table('pages', function(Blueprint $table) {
+ $table->boolean('draft')->default(false);
+ $table->index('draft');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('images', function (Blueprint $table) {
+ $table->dropIndex(['entity_type', 'entity_id']);
+ $table->dropColumn('entity_type');
+ $table->dropColumn('entity_id');
+ });
+
+ Schema::table('pages', function (Blueprint $table) {
+ $table->dropColumn('draft');
+ });
+ }
+}
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
+
$scope.images = [];
$scope.imageType = $attrs.imageType;
$scope.selectedImage = false;
$scope.hasMore = false;
$scope.imageUpdateSuccess = false;
$scope.imageDeleteSuccess = false;
+
var page = 0;
var previousClickTime = 0;
var dataLoaded = false;
var pageId = Number($attrs.pageId);
var isEdit = pageId !== 0;
var autosaveFrequency = 30; // AutoSave interval in seconds.
- $scope.isDraft = Number($attrs.pageDraft) === 1;
- if ($scope.isDraft) $scope.draftText = 'Editing Draft';
+ $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
+ $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
+ if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
+ $scope.draftText = 'Editing Draft'
+ } else {
+ $scope.draftText = 'Editing Page'
+ };
var autoSave = false;
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml;
currentContent.title = newTitle;
- saveDraftUpdate(newTitle, newHtml);
+ saveDraft(newTitle, newHtml);
}
}, 1000 * autosaveFrequency);
}
* @param title
* @param html
*/
- function saveDraftUpdate(title, html) {
+ function saveDraft(title, html) {
$http.put('/ajax/page/' + pageId + '/save-draft', {
name: title,
html: html
}).then((responseData) => {
$scope.draftText = responseData.data.message;
- $scope.isDraft = true;
+ if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
});
}
+ $scope.forceDraftSave = function() {
+ var newTitle = $('#name').val();
+ var newHtml = $scope.editorHtml;
+ saveDraft(newTitle, newHtml);
+ };
+
/**
* Discard the current draft and grab the current page
* content from the system via an AJAX request.
$scope.discardDraft = function () {
$http.get('/ajax/page/' + pageId).then((responseData) => {
if (autoSave) $interval.cancel(autoSave);
- $scope.draftText = '';
- $scope.isDraft = false;
+ $scope.draftText = 'Editing Page';
+ $scope.isUpdateDraft = false;
$scope.$broadcast('html-update', responseData.data.html);
- $('#name').val(currentContent.title);
+ $('#name').val(responseData.data.name);
$timeout(() => {
startAutoSave();
}, 1000);
.faded span.faded-text {
display: inline-block;
padding: $-s;
- opacity: 0.5;
}
.faded-small {
.page {
border-left: 5px solid $color-page;
}
+ .page.draft {
+ border-left: 5px solid $color-page-draft;
+ .text-page {
+ color: $color-page-draft;
+ }
+ }
.chapter {
border-left: 5px solid $color-chapter;
}
background-color: rgba($color-page, 0.1);
}
}
+ .list-item-page.draft {
+ border-left: 5px solid $color-page-draft;
+ }
+ .page.draft .page, .list-item-page.draft a.page {
+ color: $color-page-draft !important;
+ }
.sub-menu {
display: none;
padding-left: 0;
position: absolute;
}
-
.activity-list-item {
padding: $-s 0;
color: #888;
font-size: 0.75em;
margin-top: $-xs;
}
+ .page.draft .text-page {
+ color: $color-page-draft;
+ }
}
.entity-list.compact {
font-size: 0.6em;
$color-book: #009688;
$color-chapter: #ef7c3c;
$color-page: $primary;
+$color-page-draft: #9A60DA;
// Text colours
$text-dark: #444;
<div class="row">
<div class="col-md-4 faded">
<div class="breadcrumbs">
- <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->name }}</a>
+ <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>
</div>
<div class="col-md-8 faded">
<div class="row">
<div class="col-sm-4">
+ <div id="recent-drafts">
+ @if(count($draftPages) > 0)
+ <h3>My Recent Drafts</h3>
+ @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
+ @endif
+ </div>
@if($signedIn)
<h3>My Recently Viewed</h3>
@else
@extends('base')
@section('head')
- <script src="/libs/tinymce/tinymce.min.js?ver=4.3.2"></script>
+ <script src="/libs/tinymce/tinymce.min.js?ver=4.3.7"></script>
@stop
@section('body-class', 'flexbox')
@section('content')
<div class="flex-fill flex">
- <form action="{{$book->getUrl() . '/page'}}" method="POST" class="flex flex-fill">
- @include('pages/form')
- @if($chapter)
- <input type="hidden" name="chapter" value="{{$chapter->id}}">
- @endif
+ <form action="{{$book->getUrl() . '/page/' . $draft->id}}" method="POST" class="flex flex-fill">
+ @include('pages/form', ['model' => $draft])
</form>
</div>
@include('partials/image-manager', ['imageType' => 'gallery'])
@section('content')
<div class="container small" ng-non-bindable>
- <h1>Delete Page</h1>
- <p class="text-neg">Are you sure you want to delete this page?</p>
+ <h1>Delete {{ $page->draft ? 'Draft' : '' }} Page</h1>
+ <p class="text-neg">Are you sure you want to delete this {{ $page->draft ? 'draft' : '' }} page?</p>
<form action="{{$page->getUrl()}}" method="POST">
{!! csrf_field() !!}
@extends('base')
@section('head')
- <script src="/libs/tinymce/tinymce.min.js?ver=4.3.2"></script>
+ <script src="/libs/tinymce/tinymce.min.js?ver=4.3.7"></script>
@stop
@section('body-class', 'flexbox')
-
-
-<div class="page-editor flex-fill flex" ng-controller="PageEditController" page-id="{{ $model->id or 0 }}" page-draft="{{ $page->isDraft or 0 }}">
+<div class="page-editor flex-fill flex" ng-controller="PageEditController" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
{{ csrf_field() }}
<div class="faded-small toolbar">
</div>
</div>
<div class="col-sm-4 faded text-center">
- <span class="faded-text" ng-bind="draftText"></span>
+
+ <div dropdown class="dropdown-container">
+ <a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span> <i class="zmdi zmdi-more-vert"></i></a>
+ <ul>
+ <li>
+ <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
+ </li>
+ <li ng-if="isNewPageDraft">
+ <a href="{{$model->getUrl()}}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
+ </li>
+ </ul>
+ </div>
</div>
<div class="col-sm-4 faded">
<div class="action-buttons" ng-cloak>
- <button type="button" ng-if="isDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button>
+
+ <button type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button>
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
</div>
</div>
-<div class="page">
+<div class="page {{$page->draft ? 'draft' : ''}}">
<h3>
<a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a>
</h3>
@foreach($sidebarTree as $bookChild)
- <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }}">
+ <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
<a href="{{$bookChild->getUrl()}}" class="{{ $bookChild->getClassName() }} {{ $current->matches($bookChild)? 'selected' : '' }}">
@if($bookChild->isA('chapter'))<i class="zmdi zmdi-collection-bookmark"></i>@else <i class="zmdi zmdi-file-text"></i>@endif{{ $bookChild->name }}
</a>
</p>
<ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">
@foreach($bookChild->pages as $childPage)
- <li class="list-item-page">
+ <li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}">
<a href="{{$childPage->getUrl()}}" class="page {{ $current->matches($childPage)? 'selected' : '' }}">
<i class="zmdi zmdi-file-text"></i> {{ $childPage->name }}
</a>
$this->asAdmin()
// Navigate to page create form
->visit($chapter->getUrl())
- ->click('New Page')
- ->seePageIs($chapter->getUrl() . '/create-page')
+ ->click('New Page');
+
+ $draftPage = \BookStack\Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
+
+ $this->seePageIs($draftPage->getUrl())
// Fill out form
->type($page->name, '#name')
->type($page->html, '#html')
<?php
-class PageUpdateDraftTest extends TestCase
+class PageDraftTest extends TestCase
{
protected $page;
protected $pageRepo;
->see('Admin has started editing this page');
}
+ public function test_draft_pages_show_on_homepage()
+ {
+ $book = \BookStack\Book::first();
+ $this->asAdmin()->visit('/')
+ ->dontSeeInElement('#recent-drafts', 'New Page')
+ ->visit($book->getUrl() . '/page/create')
+ ->visit('/')
+ ->seeInElement('#recent-drafts', 'New Page');
+ }
+
+ public function test_draft_pages_not_visible_by_others()
+ {
+ $book = \BookStack\Book::first();
+ $chapter = $book->chapters->first();
+ $newUser = $this->getNewUser();
+
+ $this->actingAs($newUser)->visit('/')
+ ->visit($book->getUrl() . '/page/create')
+ ->visit($chapter->getUrl() . '/create-page')
+ ->visit($book->getUrl())
+ ->seeInElement('.page-list', 'New Page');
+
+ $this->asAdmin()
+ ->visit($book->getUrl())
+ ->dontSeeInElement('.page-list', 'New Page')
+ ->visit($chapter->getUrl())
+ ->dontSeeInElement('.page-list', 'New Page');
+ }
+
}
$baseUrl = $ownBook->getUrl() . '/page';
- $this->checkAccessPermission('page-create-own', [
- $baseUrl . '/create',
- $ownChapter->getUrl() . '/create-page'
- ], [
+ $createUrl = $baseUrl . '/create';
+ $createUrlChapter = $ownChapter->getUrl() . '/create-page';
+ $accessUrls = [$createUrl, $createUrlChapter];
+
+ foreach ($accessUrls as $url) {
+ $this->actingAs($this->user)->visit('/')->visit($url)
+ ->seePageIs('/');
+ }
+
+ $this->checkAccessPermission('page-create-own', [], [
$ownBook->getUrl() => 'New Page',
$ownChapter->getUrl() => 'New Page'
]);
+ $this->giveUserPermissions($this->user, ['page-create-own']);
+
+ foreach ($accessUrls as $index => $url) {
+ $this->actingAs($this->user)->visit('/')->visit($url);
+ $expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+ $this->seePageIs($expectedUrl);
+ }
+
$this->visit($baseUrl . '/create')
->type('test page', 'name')
->type('page desc', 'html')
$book = \BookStack\Book::take(1)->get()->first();
$chapter = \BookStack\Chapter::take(1)->get()->first();
$baseUrl = $book->getUrl() . '/page';
- $this->checkAccessPermission('page-create-all', [
- $baseUrl . '/create',
- $chapter->getUrl() . '/create-page'
- ], [
+ $createUrl = $baseUrl . '/create';
+
+ $createUrlChapter = $chapter->getUrl() . '/create-page';
+ $accessUrls = [$createUrl, $createUrlChapter];
+
+ foreach ($accessUrls as $url) {
+ $this->actingAs($this->user)->visit('/')->visit($url)
+ ->seePageIs('/');
+ }
+
+ $this->checkAccessPermission('page-create-all', [], [
$book->getUrl() => 'New Page',
$chapter->getUrl() => 'New Page'
]);
+ $this->giveUserPermissions($this->user, ['page-create-all']);
+
+ foreach ($accessUrls as $index => $url) {
+ $this->actingAs($this->user)->visit('/')->visit($url);
+ $expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+ $this->seePageIs($expectedUrl);
+ }
+
$this->visit($baseUrl . '/create')
->type('test page', 'name')
->type('page desc', 'html')