From 59ce228c2e01288846d823d563e31a1ba357b63d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 9 Mar 2016 22:32:07 +0000 Subject: [PATCH 1/4] Moved page editing to angular controller and started work on update drafts --- app/Http/Controllers/PageController.php | 17 +++++++ app/Http/routes.php | 3 ++ app/Page.php | 2 +- app/PageRevision.php | 16 ++++-- app/Repos/PageRepo.php | 49 ++++++++++++++++--- ...6_03_09_203143_add_page_revision_types.php | 32 ++++++++++++ resources/assets/js/controllers.js | 45 +++++++++++++++++ resources/assets/js/directives.js | 26 ++++++++++ resources/assets/js/global.js | 8 +-- resources/assets/js/pages/page-form.js | 9 +++- resources/views/pages/create.blade.php | 2 +- resources/views/pages/edit.blade.php | 4 +- resources/views/pages/form.blade.php | 11 +++-- resources/views/pages/revisions.blade.php | 10 ++-- 14 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 database/migrations/2016_03_09_203143_add_page_revision_types.php diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 19e4744ea..c6228a8bc 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -142,6 +142,23 @@ class PageController extends Controller return redirect($page->getUrl()); } + /** + * Save a draft update as a revision. + * @param Request $request + * @param $pageId + * @return \Illuminate\Http\JsonResponse + */ + public function saveUpdateDraft(Request $request, $pageId) + { + $this->validate($request, [ + 'name' => 'required|string|max:255' + ]); + $page = $this->pageRepo->getById($pageId); + $this->checkOwnablePermission('page-update', $page); + $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); + return response()->json(['status' => 'success', 'message' => 'Draft successfully saved']); + } + /** * Redirect from a special link url which * uses the page id rather than the name. diff --git a/app/Http/routes.php b/app/Http/routes.php index 81bbb16bc..e16d4f8f9 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -75,6 +75,9 @@ Route::group(['middleware' => 'auth'], function () { Route::delete('/{imageId}', 'ImageController@destroy'); }); + // Ajax routes + Route::put('/ajax/page/{id}/save-draft', 'PageController@saveUpdateDraft'); + // Links Route::get('/link/{id}', 'PageController@redirectFromLink'); diff --git a/app/Page.php b/app/Page.php index 53724ec20..34dee2f2f 100644 --- a/app/Page.php +++ b/app/Page.php @@ -34,7 +34,7 @@ class Page extends Entity public function revisions() { - return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc'); + return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc'); } public function getUrl() diff --git a/app/PageRevision.php b/app/PageRevision.php index 52c37e390..f1b4bc587 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -1,6 +1,4 @@ -belongsTo('BookStack\User', 'created_by'); } + /** + * Get the page this revision originates from. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function page() { return $this->belongsTo('BookStack\Page'); } + /** + * Get the url for this revision. + * @return string + */ public function getUrl() { return $this->page->getUrl() . '/revisions/' . $this->id; diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 4784ad407..ca97fc1e9 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -4,6 +4,7 @@ use Activity; use BookStack\Book; use BookStack\Exceptions\NotFoundException; +use DOMDocument; use Illuminate\Support\Str; use BookStack\Page; use BookStack\PageRevision; @@ -66,9 +67,10 @@ class PageRepo extends EntityRepo public function findPageUsingOldSlug($pageSlug, $bookSlug) { $revision = $this->pageRevision->where('slug', '=', $pageSlug) - ->whereHas('page', function($query) { + ->whereHas('page', function ($query) { $this->restrictionService->enforcePageRestrictions($query); }) + ->where('type', '=', 'version') ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') ->with('page')->first(); return $revision !== null ? $revision->page : null; @@ -100,8 +102,8 @@ class PageRepo extends EntityRepo * Save a new page into the system. * Input validation must be done beforehand. * @param array $input - * @param Book $book - * @param int $chapterId + * @param Book $book + * @param int $chapterId * @return Page */ public function saveNew(array $input, Book $book, $chapterId = null) @@ -128,9 +130,9 @@ class PageRepo extends EntityRepo */ protected function formatHtml($htmlText) { - if($htmlText == '') return $htmlText; + if ($htmlText == '') return $htmlText; libxml_use_internal_errors(true); - $doc = new \DOMDocument(); + $doc = new DOMDocument(); $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); $container = $doc->documentElement; @@ -239,8 +241,8 @@ class PageRepo extends EntityRepo /** * Updates a page with any fillable data and saves it into the database. - * @param Page $page - * @param int $book_id + * @param Page $page + * @param int $book_id * @param string $input * @return Page */ @@ -297,6 +299,7 @@ class PageRepo extends EntityRepo $revision->book_slug = $page->book->slug; $revision->created_by = auth()->user()->id; $revision->created_at = $page->updated_at; + $revision->type = 'version'; $revision->save(); // Clear old revisions if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { @@ -306,6 +309,36 @@ class PageRepo extends EntityRepo return $revision; } + /** + * Save a page update draft. + * @param Page $page + * @param array $data + * @return PageRevision + */ + public function saveUpdateDraft(Page $page, $data = []) + { + $userId = auth()->user()->id; + $drafts = $this->pageRevision->where('created_by', '=', $userId) + ->where('type', 'update_draft') + ->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc')->get(); + + if ($drafts->count() > 0) { + $draft = $drafts->first(); + } else { + $draft = $this->pageRevision->newInstance(); + $draft->page_id = $page->id; + $draft->slug = $page->slug; + $draft->book_slug = $page->book->slug; + $draft->created_by = $userId; + $draft->type = 'update_draft'; + } + + $draft->fill($data); + $draft->save(); + return $draft; + } + /** * Gets a single revision via it's id. * @param $id @@ -333,7 +366,7 @@ class PageRepo extends EntityRepo /** * Changes the related book for the specified page. * Changes the book id of any relations to the page that store the book id. - * @param int $bookId + * @param int $bookId * @param Page $page * @return Page */ diff --git a/database/migrations/2016_03_09_203143_add_page_revision_types.php b/database/migrations/2016_03_09_203143_add_page_revision_types.php new file mode 100644 index 000000000..e39c77d18 --- /dev/null +++ b/database/migrations/2016_03_09_203143_add_page_revision_types.php @@ -0,0 +1,32 @@ +string('type')->default('version'); + $table->index('type'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('page_revisions', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } +} diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 1f7388859..305e0c3c1 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -213,4 +213,49 @@ module.exports = function (ngApp, events) { }]); + ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', function ($scope, $http, $attrs, $interval) { + + $scope.editorOptions = require('./pages/page-form'); + $scope.editorHtml = ''; + $scope.draftText = ''; + var pageId = Number($attrs.pageId); + var isEdit = pageId !== 0; + + if (isEdit) { + startAutoSave(); + } + + $scope.editorChange = function() { + $scope.draftText = ''; + } + + function startAutoSave() { + var currentTitle = $('#name').val(); + var currentHtml = $scope.editorHtml; + + console.log('Starting auto save'); + + $interval(() => { + var newTitle = $('#name').val(); + var newHtml = $scope.editorHtml; + + if (newTitle !== currentTitle || newHtml !== currentHtml) { + currentHtml = newHtml; + currentTitle = newTitle; + saveDraftUpdate(newTitle, newHtml); + } + }, 1000*5); + } + + function saveDraftUpdate(title, html) { + $http.put('/ajax/page/' + pageId + '/save-draft', { + name: title, + html: html + }).then((responseData) => { + $scope.draftText = 'Draft saved' + }) + } + + }]); + }; \ No newline at end of file diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 60abde6e9..b6c41bb3c 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -162,5 +162,31 @@ module.exports = function (ngApp, events) { }; }]); + ngApp.directive('tinymce', [function() { + return { + restrict: 'A', + scope: { + tinymce: '=', + ngModel: '=', + ngChange: '=' + }, + link: function (scope, element, attrs) { + + function tinyMceSetup(editor) { + editor.on('keyup', (e) => { + var content = editor.getContent(); + scope.$apply(() => { + scope.ngModel = content; + }); + scope.ngChange(content); + }); + } + + scope.tinymce.extraSetups.push(tinyMceSetup); + tinymce.init(scope.tinymce); + } + } + }]) + }; \ No newline at end of file diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 5400a8af0..aa5e60ce4 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -119,11 +119,5 @@ function elemExists(selector) { return document.querySelector(selector) !== null; } -// TinyMCE editor -if (elemExists('#html-editor')) { - var tinyMceOptions = require('./pages/page-form'); - tinymce.init(tinyMceOptions); -} - // Page specific items -require('./pages/page-show'); \ No newline at end of file +require('./pages/page-show'); diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 290b7c653..0310b5fa2 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -1,4 +1,4 @@ -module.exports = { +var mceOptions = module.exports = { selector: '#html-editor', content_css: [ '/css/styles.css' @@ -51,8 +51,15 @@ module.exports = { args.content = ''; } }, + extraSetups: [], setup: function (editor) { + console.log(mceOptions.extraSetups); + + for (var i = 0; i < mceOptions.extraSetups.length; i++) { + mceOptions.extraSetups[i](editor); + } + (function () { var wrap; diff --git a/resources/views/pages/create.blade.php b/resources/views/pages/create.blade.php index 69c5f7c94..441379eae 100644 --- a/resources/views/pages/create.blade.php +++ b/resources/views/pages/create.blade.php @@ -8,7 +8,7 @@ @section('content') -
+
@include('pages/form') @if($chapter) diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index 6dde47c63..0832f63b4 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -8,8 +8,8 @@ @section('content') -
- +
+ @include('pages/form', ['model' => $page]) diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index f1f54d97f..e32ef537a 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,7 +1,7 @@ -
+
{{ csrf_field() }}
@@ -12,7 +12,10 @@ Toggle Header
-
+
+ +
+
Cancel @@ -22,13 +25,13 @@
-
+
@include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
- @if($errors->has('html'))
{{ $errors->first('html') }}
diff --git a/resources/views/pages/revisions.blade.php b/resources/views/pages/revisions.blade.php index e3782ef6e..a73f16a4f 100644 --- a/resources/views/pages/revisions.blade.php +++ b/resources/views/pages/revisions.blade.php @@ -24,10 +24,10 @@ - - - - + + + + @foreach($page->revisions as $revision) @@ -38,7 +38,7 @@ @endif - +
NameCreated ByRevision DateActionsNameCreated ByRevision DateActions
@if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif{{$revision->created_at->format('jS F, Y H:i:s')}} ({{$revision->created_at->diffForHumans()}}){{$revision->created_at->format('jS F, Y H:i:s')}}
({{$revision->created_at->diffForHumans()}})
Preview  |  From 93ebdf724b9cd051b3bfb3bd1dcc748b0c7fb714 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 9 Mar 2016 22:54:18 +0000 Subject: [PATCH 2/4] Changed direct attributes to prevent conflicts --- resources/assets/js/directives.js | 9 +++++---- resources/views/pages/form.blade.php | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index b6c41bb3c..dee02ab40 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -167,18 +167,19 @@ module.exports = function (ngApp, events) { restrict: 'A', scope: { tinymce: '=', - ngModel: '=', - ngChange: '=' + mceModel: '=', + mceChange: '=' }, link: function (scope, element, attrs) { function tinyMceSetup(editor) { editor.on('keyup', (e) => { var content = editor.getContent(); + console.log(content); scope.$apply(() => { - scope.ngModel = content; + scope.mceModel = content; }); - scope.ngChange(content); + scope.mceChange(content); }); } diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index e32ef537a..f406247d7 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -31,7 +31,7 @@
- @if($errors->has('html'))
{{ $errors->first('html') }}
From 30214fde74c954ee6cf4daeb562764343b546b58 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 12 Mar 2016 15:52:19 +0000 Subject: [PATCH 3/4] Added UI components of page autosaving --- app/Http/Controllers/PageController.php | 34 +++++- app/Http/routes.php | 1 + app/Repos/PageRepo.php | 114 +++++++++++++++++- resources/assets/js/controllers.js | 68 ++++++++--- resources/assets/js/directives.js | 18 ++- resources/assets/js/global.js | 6 +- resources/assets/js/pages/page-form.js | 2 - resources/assets/sass/_header.scss | 6 + resources/assets/sass/_variables.scss | 1 + resources/assets/sass/styles.scss | 4 + resources/views/pages/form.blade.php | 10 +- .../views/partials/notifications.blade.php | 8 +- 12 files changed, 237 insertions(+), 35 deletions(-) diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index c6228a8bc..04065996e 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -107,6 +107,17 @@ class PageController extends Controller return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]); } + /** + * Get page from an ajax request. + * @param $pageId + * @return \Illuminate\Http\JsonResponse + */ + public function getPageAjax($pageId) + { + $page = $this->pageRepo->getById($pageId); + return response()->json($page); + } + /** * Show the form for editing the specified page. * @param $bookSlug @@ -119,6 +130,24 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($pageSlug, $book->id); $this->checkOwnablePermission('page-update', $page); $this->setPageTitle('Editing Page ' . $page->getShortName()); + $page->isDraft = false; + + // Check for active editing and drafts + $warnings = []; + if ($this->pageRepo->isPageEditingActive($page, 60)) { + $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60); + } + + if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { + $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); + $page->name = $draft->name; + $page->html = $draft->html; + $page->isDraft = true; + $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft); + } + + if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings)); + return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); } @@ -155,8 +184,9 @@ class PageController extends Controller ]); $page = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-update', $page); - $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); - return response()->json(['status' => 'success', 'message' => 'Draft successfully saved']); + $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]); } /** diff --git a/app/Http/routes.php b/app/Http/routes.php index e16d4f8f9..48765be88 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -77,6 +77,7 @@ Route::group(['middleware' => 'auth'], function () { // Ajax routes Route::put('/ajax/page/{id}/save-draft', 'PageController@saveUpdateDraft'); + Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); // Links Route::get('/link/{id}', 'PageController@redirectFromLink'); diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index ca97fc1e9..776d1eadf 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -4,6 +4,7 @@ use Activity; use BookStack\Book; use BookStack\Exceptions\NotFoundException; +use Carbon\Carbon; use DOMDocument; use Illuminate\Support\Str; use BookStack\Page; @@ -259,11 +260,16 @@ class PageRepo extends EntityRepo } // Update with new details + $userId = auth()->user()->id; $page->fill($input); $page->html = $this->formatHtml($input['html']); $page->text = strip_tags($page->html); - $page->updated_by = auth()->user()->id; + $page->updated_by = $userId; $page->save(); + + // Remove all update drafts for this user & page. + $this->userUpdateDraftsQuery($page, $userId)->delete(); + return $page; } @@ -318,10 +324,7 @@ class PageRepo extends EntityRepo public function saveUpdateDraft(Page $page, $data = []) { $userId = auth()->user()->id; - $drafts = $this->pageRevision->where('created_by', '=', $userId) - ->where('type', 'update_draft') - ->where('page_id', '=', $page->id) - ->orderBy('created_at', 'desc')->get(); + $drafts = $this->userUpdateDraftsQuery($page, $userId)->get(); if ($drafts->count() > 0) { $draft = $drafts->first(); @@ -339,6 +342,107 @@ class PageRepo extends EntityRepo return $draft; } + /** + * The base query for getting user update drafts. + * @param Page $page + * @param $userId + * @return mixed + */ + private function userUpdateDraftsQuery(Page $page, $userId) + { + return $this->pageRevision->where('created_by', '=', $userId) + ->where('type', 'update_draft') + ->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc'); + } + + /** + * Checks whether a user has a draft version of a particular page or not. + * @param Page $page + * @param $userId + * @return bool + */ + public function hasUserGotPageDraft(Page $page, $userId) + { + return $this->userUpdateDraftsQuery($page, $userId)->count() > 0; + } + + /** + * Get the latest updated draft revision for a particular page and user. + * @param Page $page + * @param $userId + * @return mixed + */ + public function getUserPageDraft(Page $page, $userId) + { + return $this->userUpdateDraftsQuery($page, $userId)->first(); + } + + /** + * Get the notification message that informs the user that they are editing a draft page. + * @param PageRevision $draft + * @return string + */ + public function getUserPageDraftMessage(PageRevision $draft) + { + $message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.'; + if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) { + $message .= "\n This page has been updated by since that time. It is recommended that you discard this draft."; + } + return $message; + } + + /** + * Check if a page is being actively editing. + * Checks for edits since last page updated. + * Passing in a minuted range will check for edits + * within the last x minutes. + * @param Page $page + * @param null $minRange + * @return bool + */ + public function isPageEditingActive(Page $page, $minRange = null) + { + $draftSearch = $this->activePageEditingQuery($page, $minRange); + return $draftSearch->count() > 0; + } + + /** + * Get a notification message concerning the editing activity on + * a particular page. + * @param Page $page + * @param null $minRange + * @return string + */ + public function getPageEditingActiveMessage(Page $page, $minRange = null) + { + $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get(); + $userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has'; + $timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes'; + $message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!'; + return sprintf($message, $userMessage, $timeMessage); + } + + /** + * A query to check for active update drafts on a particular page. + * @param Page $page + * @param null $minRange + * @return mixed + */ + private function activePageEditingQuery(Page $page, $minRange = null) + { + $query = $this->pageRevision->where('type', '=', 'update_draft') + ->where('updated_at', '>', $page->updated_at) + ->where('created_by', '!=', auth()->user()->id) + ->with('createdBy'); + + if ($minRange !== null) { + $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange)); + } + + return $query; + } + /** * Gets a single revision via it's id. * @param $id diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 305e0c3c1..76b8cc67d 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -213,49 +213,85 @@ module.exports = function (ngApp, events) { }]); - ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', function ($scope, $http, $attrs, $interval) { + ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', function ($scope, $http, $attrs, $interval, $timeout) { $scope.editorOptions = require('./pages/page-form'); $scope.editorHtml = ''; $scope.draftText = ''; 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'; + + var autoSave = false; + + var currentContent = { + title: false, + html: false + }; if (isEdit) { - startAutoSave(); + setTimeout(() => { + startAutoSave(); + }, 1000); } - $scope.editorChange = function() { - $scope.draftText = ''; - } + $scope.editorChange = function () {} + /** + * Start the AutoSave loop, Checks for content change + * before performing the costly AJAX request. + */ function startAutoSave() { - var currentTitle = $('#name').val(); - var currentHtml = $scope.editorHtml; + currentContent.title = $('#name').val(); + currentContent.html = $scope.editorHtml; - console.log('Starting auto save'); - - $interval(() => { + autoSave = $interval(() => { var newTitle = $('#name').val(); var newHtml = $scope.editorHtml; - if (newTitle !== currentTitle || newHtml !== currentHtml) { - currentHtml = newHtml; - currentTitle = newTitle; + if (newTitle !== currentContent.title || newHtml !== currentContent.html) { + currentContent.html = newHtml; + currentContent.title = newTitle; saveDraftUpdate(newTitle, newHtml); } - }, 1000*5); + }, 1000 * autosaveFrequency); } + /** + * Save a draft update into the system via an AJAX request. + * @param title + * @param html + */ function saveDraftUpdate(title, html) { $http.put('/ajax/page/' + pageId + '/save-draft', { name: title, html: html }).then((responseData) => { - $scope.draftText = 'Draft saved' - }) + $scope.draftText = responseData.data.message; + $scope.isDraft = true; + }); } + /** + * 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.$broadcast('html-update', responseData.data.html); + $('#name').val(currentContent.title); + $timeout(() => { + startAutoSave(); + }, 1000); + events.emit('success', 'Draft discarded, The editor has been updated with the current page content'); + }); + }; + }]); }; \ No newline at end of file diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index dee02ab40..72d35d455 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -162,7 +162,7 @@ module.exports = function (ngApp, events) { }; }]); - ngApp.directive('tinymce', [function() { + ngApp.directive('tinymce', ['$timeout', function($timeout) { return { restrict: 'A', scope: { @@ -173,14 +173,24 @@ module.exports = function (ngApp, events) { link: function (scope, element, attrs) { function tinyMceSetup(editor) { - editor.on('keyup', (e) => { + editor.on('ExecCommand change NodeChange ObjectResized', (e) => { var content = editor.getContent(); - console.log(content); - scope.$apply(() => { + $timeout(() => { scope.mceModel = content; }); scope.mceChange(content); }); + + editor.on('init', (e) => { + scope.mceModel = editor.getContent(); + }); + + scope.$on('html-update', (event, value) => { + editor.setContent(value); + editor.selection.select(editor.getBody(), true); + editor.selection.collapse(false); + scope.mceModel = editor.getContent(); + }); } scope.tinymce.extraSetups.push(tinyMceSetup); diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index aa5e60ce4..9e2b3b8ea 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -54,10 +54,10 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) { // Global jQuery Elements $(function () { - var notifications = $('.notification'); var successNotification = notifications.filter('.pos'); var errorNotification = notifications.filter('.neg'); + var warningNotification = notifications.filter('.warning'); // Notification Events window.Events.listen('success', function (text) { successNotification.hide(); @@ -66,6 +66,10 @@ $(function () { successNotification.show(); }, 1); }); + window.Events.listen('warning', function (text) { + warningNotification.find('span').text(text); + warningNotification.show(); + }); window.Events.listen('error', function (text) { errorNotification.find('span').text(text); errorNotification.show(); diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 0310b5fa2..c6787ba87 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -54,8 +54,6 @@ var mceOptions = module.exports = { extraSetups: [], setup: function (editor) { - console.log(mceOptions.extraSetups); - for (var i = 0; i < mceOptions.extraSetups.length; i++) { mceOptions.extraSetups[i](editor); } diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index 87aa20046..938464228 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -161,6 +161,12 @@ form.search-box { } } +.faded > span.faded-text { + display: inline-block; + padding: $-s; + opacity: 0.5; +} + .faded-small { color: #000; font-size: 0.9em; diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index 874515bfd..cb6cec9c1 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -38,6 +38,7 @@ $primary-dark: #0288D1; $secondary: #e27b41; $positive: #52A256; $negative: #E84F4F; +$warning: $secondary; $primary-faded: rgba(21, 101, 192, 0.15); // Item Colors diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 9c4a4dafc..7c7821242 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -88,6 +88,10 @@ body.dragging, body.dragging * { background-color: $negative; color: #EEE; } + &.warning { + background-color: $secondary; + color: #EEE; + } } // Loading icon diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index f406247d7..b8194cda7 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,7 +1,7 @@ -
+
{{ csrf_field() }}
@@ -9,15 +9,19 @@
- +
+ + +
- Cancel +
diff --git a/resources/views/partials/notifications.blade.php b/resources/views/partials/notifications.blade.php index 8cc0774c9..183934c66 100644 --- a/resources/views/partials/notifications.blade.php +++ b/resources/views/partials/notifications.blade.php @@ -1,8 +1,12 @@ + + From bf7852ce85206889d5d8c91ecee7fba1690f6edd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 12 Mar 2016 16:31:02 +0000 Subject: [PATCH 4/4] Organised test files & added page update draft tests Also cleaned styling for new autosave ui parts. Closes #36. --- public/uploads/.gitignore | 0 resources/assets/sass/_header.scss | 5 +- resources/views/pages/form.blade.php | 9 +-- tests/{ => Entity}/EntitySearchTest.php | 0 tests/{ => Entity}/EntityTest.php | 0 tests/Entity/PageUpdateDraftTest.php | 62 ++++++++++++++++++++ tests/{ => Permissions}/RestrictionsTest.php | 0 tests/{ => Permissions}/RolesTest.php | 0 8 files changed, 69 insertions(+), 7 deletions(-) mode change 100644 => 100755 public/uploads/.gitignore rename tests/{ => Entity}/EntitySearchTest.php (100%) rename tests/{ => Entity}/EntityTest.php (100%) create mode 100644 tests/Entity/PageUpdateDraftTest.php rename tests/{ => Permissions}/RestrictionsTest.php (100%) rename tests/{ => Permissions}/RolesTest.php (100%) diff --git a/public/uploads/.gitignore b/public/uploads/.gitignore old mode 100644 new mode 100755 diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index 938464228..8fed6aef7 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -161,7 +161,7 @@ form.search-box { } } -.faded > span.faded-text { +.faded span.faded-text { display: inline-block; padding: $-s; opacity: 0.5; @@ -189,6 +189,9 @@ form.search-box { padding-left: 0; } } + &.text-center { + text-align: center; + } } .setting-nav { diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index b8194cda7..a5beabacf 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -14,14 +14,11 @@
-
- - -
+
-
- +
+
diff --git a/tests/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php similarity index 100% rename from tests/EntitySearchTest.php rename to tests/Entity/EntitySearchTest.php diff --git a/tests/EntityTest.php b/tests/Entity/EntityTest.php similarity index 100% rename from tests/EntityTest.php rename to tests/Entity/EntityTest.php diff --git a/tests/Entity/PageUpdateDraftTest.php b/tests/Entity/PageUpdateDraftTest.php new file mode 100644 index 000000000..d321974db --- /dev/null +++ b/tests/Entity/PageUpdateDraftTest.php @@ -0,0 +1,62 @@ +page = \BookStack\Page::first(); + $this->pageRepo = app('\BookStack\Repos\PageRepo'); + } + + public function test_draft_content_shows_if_available() + { + $addedContent = '

test message content

'; + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->dontSeeInField('html', $addedContent); + + $newContent = $this->page->html . $addedContent; + $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->seeInField('html', $newContent); + } + + public function test_draft_not_visible_by_others() + { + $addedContent = '

test message content

'; + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->dontSeeInField('html', $addedContent); + + $newContent = $this->page->html . $addedContent; + $newUser = $this->getNewUser(); + $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); + $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit') + ->dontSeeInField('html', $newContent); + } + + public function test_alert_message_shows_if_editing_draft() + { + $this->asAdmin(); + $this->pageRepo->saveUpdateDraft($this->page, ['html' => 'test content']); + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->see('You are currently editing a draft'); + } + + public function test_alert_message_shows_if_someone_else_editing() + { + $addedContent = '

test message content

'; + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->dontSeeInField('html', $addedContent); + + $newContent = $this->page->html . $addedContent; + $newUser = $this->getNewUser(); + $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); + $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit') + ->see('Admin has started editing this page'); + } + +} diff --git a/tests/RestrictionsTest.php b/tests/Permissions/RestrictionsTest.php similarity index 100% rename from tests/RestrictionsTest.php rename to tests/Permissions/RestrictionsTest.php diff --git a/tests/RolesTest.php b/tests/Permissions/RolesTest.php similarity index 100% rename from tests/RolesTest.php rename to tests/Permissions/RolesTest.php