Added new page drafts and started image entity attaching

Closes #80.
This commit is contained in:
Dan Brown 2016-03-13 12:04:08 +00:00
parent ced8c8e497
commit 5283919d24
26 changed files with 403 additions and 84 deletions

View File

@ -29,14 +29,17 @@ class HomeController extends Controller
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
]);
}

View File

@ -32,7 +32,6 @@ class ImageController extends Controller
parent::__construct();
}
/**
* Get all images for a specific type, Paginated
* @param int $page
@ -55,7 +54,6 @@ class ImageController extends Controller
return response()->json($imgData);
}
/**
* Handles image uploads for use on pages.
* @param string $type
@ -113,7 +111,6 @@ class ImageController extends Controller
return response()->json($image);
}
/**
* Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo

View File

@ -49,33 +49,54 @@ class PageController extends Controller
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());
}
/**
* Store a newly created page in storage.
* 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 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());
@ -132,12 +153,13 @@ class PageController extends Controller
$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;
@ -161,7 +183,7 @@ class PageController extends Controller
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);
@ -177,14 +199,15 @@ class PageController extends Controller
* @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]);
}
@ -216,9 +239,25 @@ class PageController extends Controller
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
@ -230,6 +269,24 @@ class PageController extends Controller
$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());
}
@ -295,8 +352,8 @@ class PageController extends Controller
$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'
]);
}
@ -312,8 +369,8 @@ class PageController extends Controller
$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'
]);
}
@ -329,8 +386,8 @@ class PageController extends Controller
$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'
]);
}
@ -373,7 +430,7 @@ class PageController extends Controller
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'page' => $page,
'roles' => $roles
]);
}

View File

@ -27,17 +27,20 @@ Route::group(['middleware' => 'auth'], function () {
// 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');
@ -76,8 +79,9 @@ Route::group(['middleware' => 'auth'], function () {
});
// 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');

View File

@ -40,7 +40,9 @@ class Page extends Entity
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)

View File

@ -213,15 +213,27 @@ class BookRepo extends EntityRepo
$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;
});
}
/**

View File

@ -66,7 +66,13 @@ class ChapterRepo extends EntityRepo
*/
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;
});
}
/**

View File

@ -5,6 +5,7 @@ use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
use BookStack\Services\RestrictionService;
use BookStack\User;
class EntityRepo
{
@ -79,7 +80,7 @@ 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);
}
@ -112,9 +113,24 @@ class EntityRepo
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

View File

@ -1,8 +1,8 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Exceptions\NotFoundException;
use Carbon\Carbon;
use DOMDocument;
@ -12,6 +12,7 @@ use BookStack\PageRevision;
class PageRepo extends EntityRepo
{
protected $pageRevision;
/**
@ -26,21 +27,27 @@ class PageRepo extends EntityRepo
/**
* 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);
}
/**
@ -123,6 +130,47 @@ class PageRepo extends EntityRepo
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.
@ -342,6 +390,24 @@ class PageRepo extends EntityRepo
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

View File

@ -8,15 +8,16 @@ class RestrictionService
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;
}
/**
@ -48,6 +49,16 @@ class RestrictionService
*/
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);

View File

@ -0,0 +1,44 @@
<?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');
});
}
}

View File

@ -4,6 +4,7 @@ module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
$scope.images = [];
$scope.imageType = $attrs.imageType;
$scope.selectedImage = false;
@ -12,6 +13,7 @@ module.exports = function (ngApp, events) {
$scope.hasMore = false;
$scope.imageUpdateSuccess = false;
$scope.imageDeleteSuccess = false;
var page = 0;
var previousClickTime = 0;
var dataLoaded = false;
@ -221,8 +223,13 @@ module.exports = function (ngApp, events) {
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;
@ -254,7 +261,7 @@ module.exports = function (ngApp, events) {
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml;
currentContent.title = newTitle;
saveDraftUpdate(newTitle, newHtml);
saveDraft(newTitle, newHtml);
}
}, 1000 * autosaveFrequency);
}
@ -264,16 +271,22 @@ module.exports = function (ngApp, events) {
* @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.
@ -281,10 +294,10 @@ module.exports = function (ngApp, events) {
$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);

View File

@ -164,7 +164,6 @@ form.search-box {
.faded span.faded-text {
display: inline-block;
padding: $-s;
opacity: 0.5;
}
.faded-small {

View File

@ -26,6 +26,12 @@
.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;
}
@ -182,6 +188,12 @@
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;
@ -234,7 +246,6 @@
position: absolute;
}
.activity-list-item {
padding: $-s 0;
color: #888;
@ -304,6 +315,9 @@ ul.pagination {
font-size: 0.75em;
margin-top: $-xs;
}
.page.draft .text-page {
color: $color-page-draft;
}
}
.entity-list.compact {
font-size: 0.6em;

View File

@ -45,6 +45,7 @@ $primary-faded: rgba(21, 101, 192, 0.15);
$color-book: #009688;
$color-chapter: #ef7c3c;
$color-page: $primary;
$color-page-draft: #9A60DA;
// Text colours
$text-dark: #444;

View File

@ -7,7 +7,7 @@
<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">

View File

@ -23,6 +23,12 @@
<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

View File

@ -1,7 +1,7 @@
@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')
@ -9,11 +9,8 @@
@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'])

View File

@ -3,8 +3,8 @@
@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() !!}

View File

@ -1,7 +1,7 @@
@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')

View File

@ -1,7 +1,5 @@
<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">
@ -14,11 +12,23 @@
</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>&nbsp; <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>

View File

@ -1,4 +1,4 @@
<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>

View File

@ -6,7 +6,7 @@
@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>
@ -17,7 +17,7 @@
</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>

View File

@ -88,8 +88,11 @@ class EntityTest extends TestCase
$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')

View File

@ -1,7 +1,7 @@
<?php
class PageUpdateDraftTest extends TestCase
class PageDraftTest extends TestCase
{
protected $page;
protected $pageRepo;
@ -59,4 +59,33 @@ class PageUpdateDraftTest extends TestCase
->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');
}
}

View File

@ -392,14 +392,28 @@ class RolesTest extends TestCase
$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')
@ -421,14 +435,29 @@ class RolesTest extends TestCase
$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')