mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-31 12:11:37 +01:00
parent
ced8c8e497
commit
5283919d24
@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -49,20 +49,38 @@ 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.
|
||||
* @param Request $request
|
||||
* 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 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'
|
||||
@ -70,12 +88,15 @@ class PageController extends Controller
|
||||
|
||||
$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;
|
||||
@ -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);
|
||||
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());
|
||||
}
|
||||
@ -296,7 +353,7 @@ class PageController extends Controller
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
@ -313,7 +370,7 @@ class PageController extends Controller
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
@ -330,7 +387,7 @@ class PageController extends Controller
|
||||
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -164,7 +164,6 @@ form.search-box {
|
||||
.faded span.faded-text {
|
||||
display: inline-block;
|
||||
padding: $-s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.faded-small {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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() !!}
|
||||
|
@ -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')
|
||||
|
@ -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> <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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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')
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user