Dan Brown

Added new page drafts and started image entity attaching

Closes #80.
...@@ -29,14 +29,17 @@ class HomeController extends Controller ...@@ -29,14 +29,17 @@ class HomeController extends Controller
29 public function index() 29 public function index()
30 { 30 {
31 $activity = Activity::latest(10); 31 $activity = Activity::latest(10);
32 - $recents = $this->signedIn ? Views::getUserRecentlyViewed(12, 0) : $this->entityRepo->getRecentlyCreatedBooks(10); 32 + $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
33 + $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
34 + $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor);
33 $recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5); 35 $recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
34 $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5); 36 $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
35 return view('home', [ 37 return view('home', [
36 'activity' => $activity, 38 'activity' => $activity,
37 'recents' => $recents, 39 'recents' => $recents,
38 'recentlyCreatedPages' => $recentlyCreatedPages, 40 'recentlyCreatedPages' => $recentlyCreatedPages,
39 - 'recentlyUpdatedPages' => $recentlyUpdatedPages 41 + 'recentlyUpdatedPages' => $recentlyUpdatedPages,
42 + 'draftPages' => $draftPages
40 ]); 43 ]);
41 } 44 }
42 45
......
...@@ -32,7 +32,6 @@ class ImageController extends Controller ...@@ -32,7 +32,6 @@ class ImageController extends Controller
32 parent::__construct(); 32 parent::__construct();
33 } 33 }
34 34
35 -
36 /** 35 /**
37 * Get all images for a specific type, Paginated 36 * Get all images for a specific type, Paginated
38 * @param int $page 37 * @param int $page
...@@ -55,7 +54,6 @@ class ImageController extends Controller ...@@ -55,7 +54,6 @@ class ImageController extends Controller
55 return response()->json($imgData); 54 return response()->json($imgData);
56 } 55 }
57 56
58 -
59 /** 57 /**
60 * Handles image uploads for use on pages. 58 * Handles image uploads for use on pages.
61 * @param string $type 59 * @param string $type
...@@ -113,7 +111,6 @@ class ImageController extends Controller ...@@ -113,7 +111,6 @@ class ImageController extends Controller
113 return response()->json($image); 111 return response()->json($image);
114 } 112 }
115 113
116 -
117 /** 114 /**
118 * Deletes an image and all thumbnail/image files 115 * Deletes an image and all thumbnail/image files
119 * @param PageRepo $pageRepo 116 * @param PageRepo $pageRepo
......
...@@ -49,33 +49,54 @@ class PageController extends Controller ...@@ -49,33 +49,54 @@ class PageController extends Controller
49 public function create($bookSlug, $chapterSlug = false) 49 public function create($bookSlug, $chapterSlug = false)
50 { 50 {
51 $book = $this->bookRepo->getBySlug($bookSlug); 51 $book = $this->bookRepo->getBySlug($bookSlug);
52 - $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false; 52 + $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
53 $parent = $chapter ? $chapter : $book; 53 $parent = $chapter ? $chapter : $book;
54 $this->checkOwnablePermission('page-create', $parent); 54 $this->checkOwnablePermission('page-create', $parent);
55 $this->setPageTitle('Create New Page'); 55 $this->setPageTitle('Create New Page');
56 - return view('pages/create', ['book' => $book, 'chapter' => $chapter]); 56 +
57 + $draft = $this->pageRepo->getDraftPage($book, $chapter);
58 + return redirect($draft->getUrl());
59 + }
60 +
61 + /**
62 + * Show form to continue editing a draft page.
63 + * @param $bookSlug
64 + * @param $pageId
65 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
66 + */
67 + public function editDraft($bookSlug, $pageId)
68 + {
69 + $book = $this->bookRepo->getBySlug($bookSlug);
70 + $draft = $this->pageRepo->getById($pageId, true);
71 + $this->checkOwnablePermission('page-create', $draft);
72 + $this->setPageTitle('Edit Page Draft');
73 +
74 + return view('pages/create', ['draft' => $draft, 'book' => $book]);
57 } 75 }
58 76
59 /** 77 /**
60 - * Store a newly created page in storage. 78 + * Store a new page by changing a draft into a page.
61 * @param Request $request 79 * @param Request $request
62 - * @param $bookSlug 80 + * @param string $bookSlug
63 * @return Response 81 * @return Response
64 */ 82 */
65 - public function store(Request $request, $bookSlug) 83 + public function store(Request $request, $bookSlug, $pageId)
66 { 84 {
67 $this->validate($request, [ 85 $this->validate($request, [
68 - 'name' => 'required|string|max:255' 86 + 'name' => 'required|string|max:255'
69 ]); 87 ]);
70 88
71 $input = $request->all(); 89 $input = $request->all();
72 $book = $this->bookRepo->getBySlug($bookSlug); 90 $book = $this->bookRepo->getBySlug($bookSlug);
73 - $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
74 - $parent = $chapterId !== null ? $this->chapterRepo->getById($chapterId) : $book;
75 - $this->checkOwnablePermission('page-create', $parent);
76 $input['priority'] = $this->bookRepo->getNewPriority($book); 91 $input['priority'] = $this->bookRepo->getNewPriority($book);
77 92
78 - $page = $this->pageRepo->saveNew($input, $book, $chapterId); 93 + $draftPage = $this->pageRepo->getById($pageId, true);
94 +
95 + $chapterId = $draftPage->chapter_id;
96 + $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
97 + $this->checkOwnablePermission('page-create', $parent);
98 +
99 + $page = $this->pageRepo->publishDraft($draftPage, $input);
79 100
80 Activity::add($page, 'page_create', $book->id); 101 Activity::add($page, 'page_create', $book->id);
81 return redirect($page->getUrl()); 102 return redirect($page->getUrl());
...@@ -132,12 +153,13 @@ class PageController extends Controller ...@@ -132,12 +153,13 @@ class PageController extends Controller
132 $this->setPageTitle('Editing Page ' . $page->getShortName()); 153 $this->setPageTitle('Editing Page ' . $page->getShortName());
133 $page->isDraft = false; 154 $page->isDraft = false;
134 155
135 - // Check for active editing and drafts 156 + // Check for active editing
136 $warnings = []; 157 $warnings = [];
137 if ($this->pageRepo->isPageEditingActive($page, 60)) { 158 if ($this->pageRepo->isPageEditingActive($page, 60)) {
138 $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60); 159 $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
139 } 160 }
140 161
162 + // Check for a current draft version for this user
141 if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { 163 if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
142 $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); 164 $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
143 $page->name = $draft->name; 165 $page->name = $draft->name;
...@@ -161,7 +183,7 @@ class PageController extends Controller ...@@ -161,7 +183,7 @@ class PageController extends Controller
161 public function update(Request $request, $bookSlug, $pageSlug) 183 public function update(Request $request, $bookSlug, $pageSlug)
162 { 184 {
163 $this->validate($request, [ 185 $this->validate($request, [
164 - 'name' => 'required|string|max:255' 186 + 'name' => 'required|string|max:255'
165 ]); 187 ]);
166 $book = $this->bookRepo->getBySlug($bookSlug); 188 $book = $this->bookRepo->getBySlug($bookSlug);
167 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 189 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
...@@ -177,14 +199,15 @@ class PageController extends Controller ...@@ -177,14 +199,15 @@ class PageController extends Controller
177 * @param $pageId 199 * @param $pageId
178 * @return \Illuminate\Http\JsonResponse 200 * @return \Illuminate\Http\JsonResponse
179 */ 201 */
180 - public function saveUpdateDraft(Request $request, $pageId) 202 + public function saveDraft(Request $request, $pageId)
181 { 203 {
182 - $this->validate($request, [ 204 + $page = $this->pageRepo->getById($pageId, true);
183 - 'name' => 'required|string|max:255'
184 - ]);
185 - $page = $this->pageRepo->getById($pageId);
186 $this->checkOwnablePermission('page-update', $page); 205 $this->checkOwnablePermission('page-update', $page);
187 - $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); 206 + if ($page->draft) {
207 + $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html']));
208 + } else {
209 + $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html']));
210 + }
188 $updateTime = $draft->updated_at->format('H:i'); 211 $updateTime = $draft->updated_at->format('H:i');
189 return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]); 212 return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
190 } 213 }
...@@ -216,9 +239,25 @@ class PageController extends Controller ...@@ -216,9 +239,25 @@ class PageController extends Controller
216 return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); 239 return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
217 } 240 }
218 241
242 +
243 + /**
244 + * Show the deletion page for the specified page.
245 + * @param $bookSlug
246 + * @param $pageId
247 + * @return \Illuminate\View\View
248 + * @throws NotFoundException
249 + */
250 + public function showDeleteDraft($bookSlug, $pageId)
251 + {
252 + $book = $this->bookRepo->getBySlug($bookSlug);
253 + $page = $this->pageRepo->getById($pageId, true);
254 + $this->checkOwnablePermission('page-update', $page);
255 + $this->setPageTitle('Delete Draft Page ' . $page->getShortName());
256 + return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
257 + }
258 +
219 /** 259 /**
220 * Remove the specified page from storage. 260 * Remove the specified page from storage.
221 - *
222 * @param $bookSlug 261 * @param $bookSlug
223 * @param $pageSlug 262 * @param $pageSlug
224 * @return Response 263 * @return Response
...@@ -230,6 +269,24 @@ class PageController extends Controller ...@@ -230,6 +269,24 @@ class PageController extends Controller
230 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 269 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
231 $this->checkOwnablePermission('page-delete', $page); 270 $this->checkOwnablePermission('page-delete', $page);
232 Activity::addMessage('page_delete', $book->id, $page->name); 271 Activity::addMessage('page_delete', $book->id, $page->name);
272 + session()->flash('success', 'Page deleted');
273 + $this->pageRepo->destroy($page);
274 + return redirect($book->getUrl());
275 + }
276 +
277 + /**
278 + * Remove the specified draft page from storage.
279 + * @param $bookSlug
280 + * @param $pageId
281 + * @return Response
282 + * @throws NotFoundException
283 + */
284 + public function destroyDraft($bookSlug, $pageId)
285 + {
286 + $book = $this->bookRepo->getBySlug($bookSlug);
287 + $page = $this->pageRepo->getById($pageId, true);
288 + $this->checkOwnablePermission('page-update', $page);
289 + session()->flash('success', 'Draft deleted');
233 $this->pageRepo->destroy($page); 290 $this->pageRepo->destroy($page);
234 return redirect($book->getUrl()); 291 return redirect($book->getUrl());
235 } 292 }
...@@ -295,8 +352,8 @@ class PageController extends Controller ...@@ -295,8 +352,8 @@ class PageController extends Controller
295 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 352 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
296 $pdfContent = $this->exportService->pageToPdf($page); 353 $pdfContent = $this->exportService->pageToPdf($page);
297 return response()->make($pdfContent, 200, [ 354 return response()->make($pdfContent, 200, [
298 - 'Content-Type' => 'application/octet-stream', 355 + 'Content-Type' => 'application/octet-stream',
299 - 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf' 356 + 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
300 ]); 357 ]);
301 } 358 }
302 359
...@@ -312,8 +369,8 @@ class PageController extends Controller ...@@ -312,8 +369,8 @@ class PageController extends Controller
312 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 369 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
313 $containedHtml = $this->exportService->pageToContainedHtml($page); 370 $containedHtml = $this->exportService->pageToContainedHtml($page);
314 return response()->make($containedHtml, 200, [ 371 return response()->make($containedHtml, 200, [
315 - 'Content-Type' => 'application/octet-stream', 372 + 'Content-Type' => 'application/octet-stream',
316 - 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html' 373 + 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
317 ]); 374 ]);
318 } 375 }
319 376
...@@ -329,8 +386,8 @@ class PageController extends Controller ...@@ -329,8 +386,8 @@ class PageController extends Controller
329 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 386 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
330 $containedHtml = $this->exportService->pageToPlainText($page); 387 $containedHtml = $this->exportService->pageToPlainText($page);
331 return response()->make($containedHtml, 200, [ 388 return response()->make($containedHtml, 200, [
332 - 'Content-Type' => 'application/octet-stream', 389 + 'Content-Type' => 'application/octet-stream',
333 - 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt' 390 + 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
334 ]); 391 ]);
335 } 392 }
336 393
...@@ -373,7 +430,7 @@ class PageController extends Controller ...@@ -373,7 +430,7 @@ class PageController extends Controller
373 $this->checkOwnablePermission('restrictions-manage', $page); 430 $this->checkOwnablePermission('restrictions-manage', $page);
374 $roles = $this->userRepo->getRestrictableRoles(); 431 $roles = $this->userRepo->getRestrictableRoles();
375 return view('pages/restrictions', [ 432 return view('pages/restrictions', [
376 - 'page' => $page, 433 + 'page' => $page,
377 'roles' => $roles 434 'roles' => $roles
378 ]); 435 ]);
379 } 436 }
......
...@@ -27,17 +27,20 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -27,17 +27,20 @@ Route::group(['middleware' => 'auth'], function () {
27 27
28 // Pages 28 // Pages
29 Route::get('/{bookSlug}/page/create', 'PageController@create'); 29 Route::get('/{bookSlug}/page/create', 'PageController@create');
30 - Route::post('/{bookSlug}/page', 'PageController@store'); 30 + Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
31 + Route::post('/{bookSlug}/page/{pageId}', 'PageController@store');
31 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 32 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
32 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf'); 33 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
33 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); 34 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
34 Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); 35 Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
35 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); 36 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
36 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); 37 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
38 + Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
37 Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict'); 39 Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict');
38 Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict'); 40 Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
39 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); 41 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
40 Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy'); 42 Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
43 + Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
41 44
42 // Revisions 45 // Revisions
43 Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions'); 46 Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
...@@ -76,8 +79,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -76,8 +79,9 @@ Route::group(['middleware' => 'auth'], function () {
76 }); 79 });
77 80
78 // Ajax routes 81 // Ajax routes
79 - Route::put('/ajax/page/{id}/save-draft', 'PageController@saveUpdateDraft'); 82 + Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
80 Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); 83 Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
84 + Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
81 85
82 // Links 86 // Links
83 Route::get('/link/{id}', 'PageController@redirectFromLink'); 87 Route::get('/link/{id}', 'PageController@redirectFromLink');
......
...@@ -40,7 +40,9 @@ class Page extends Entity ...@@ -40,7 +40,9 @@ class Page extends Entity
40 public function getUrl() 40 public function getUrl()
41 { 41 {
42 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; 42 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
43 - return '/books/' . $bookSlug . '/page/' . $this->slug; 43 + $midText = $this->draft ? '/draft/' : '/page/';
44 + $idComponent = $this->draft ? $this->id : $this->slug;
45 + return '/books/' . $bookSlug . $midText . $idComponent;
44 } 46 }
45 47
46 public function getExcerpt($length = 100) 48 public function getExcerpt($length = 100)
......
...@@ -213,15 +213,27 @@ class BookRepo extends EntityRepo ...@@ -213,15 +213,27 @@ class BookRepo extends EntityRepo
213 $chapters = $chapterQuery->get(); 213 $chapters = $chapterQuery->get();
214 $children = $pages->merge($chapters); 214 $children = $pages->merge($chapters);
215 $bookSlug = $book->slug; 215 $bookSlug = $book->slug;
216 +
216 $children->each(function ($child) use ($bookSlug) { 217 $children->each(function ($child) use ($bookSlug) {
217 $child->setAttribute('bookSlug', $bookSlug); 218 $child->setAttribute('bookSlug', $bookSlug);
218 if ($child->isA('chapter')) { 219 if ($child->isA('chapter')) {
219 $child->pages->each(function ($page) use ($bookSlug) { 220 $child->pages->each(function ($page) use ($bookSlug) {
220 $page->setAttribute('bookSlug', $bookSlug); 221 $page->setAttribute('bookSlug', $bookSlug);
221 }); 222 });
223 + $child->pages = $child->pages->sortBy(function($child, $key) {
224 + $score = $child->priority;
225 + if ($child->draft) $score -= 100;
226 + return $score;
227 + });
222 } 228 }
223 }); 229 });
224 - return $children->sortBy('priority'); 230 +
231 + // Sort items with drafts first then by priority.
232 + return $children->sortBy(function($child, $key) {
233 + $score = $child->priority;
234 + if ($child->isA('page') && $child->draft) $score -= 100;
235 + return $score;
236 + });
225 } 237 }
226 238
227 /** 239 /**
......
...@@ -66,7 +66,13 @@ class ChapterRepo extends EntityRepo ...@@ -66,7 +66,13 @@ class ChapterRepo extends EntityRepo
66 */ 66 */
67 public function getChildren(Chapter $chapter) 67 public function getChildren(Chapter $chapter)
68 { 68 {
69 - return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get(); 69 + $pages = $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
70 + // Sort items with drafts first then by priority.
71 + return $pages->sortBy(function($child, $key) {
72 + $score = $child->priority;
73 + if ($child->draft) $score -= 100;
74 + return $score;
75 + });
70 } 76 }
71 77
72 /** 78 /**
......
...@@ -5,6 +5,7 @@ use BookStack\Chapter; ...@@ -5,6 +5,7 @@ use BookStack\Chapter;
5 use BookStack\Entity; 5 use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 use BookStack\Services\RestrictionService; 7 use BookStack\Services\RestrictionService;
8 +use BookStack\User;
8 9
9 class EntityRepo 10 class EntityRepo
10 { 11 {
...@@ -79,7 +80,7 @@ class EntityRepo ...@@ -79,7 +80,7 @@ class EntityRepo
79 public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false) 80 public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
80 { 81 {
81 $query = $this->restrictionService->enforcePageRestrictions($this->page) 82 $query = $this->restrictionService->enforcePageRestrictions($this->page)
82 - ->orderBy('created_at', 'desc'); 83 + ->orderBy('created_at', 'desc')->where('draft', '=', false);
83 if ($additionalQuery !== false && is_callable($additionalQuery)) { 84 if ($additionalQuery !== false && is_callable($additionalQuery)) {
84 $additionalQuery($query); 85 $additionalQuery($query);
85 } 86 }
...@@ -112,10 +113,25 @@ class EntityRepo ...@@ -112,10 +113,25 @@ class EntityRepo
112 public function getRecentlyUpdatedPages($count = 20, $page = 0) 113 public function getRecentlyUpdatedPages($count = 20, $page = 0)
113 { 114 {
114 return $this->restrictionService->enforcePageRestrictions($this->page) 115 return $this->restrictionService->enforcePageRestrictions($this->page)
116 + ->where('draft', '=', false)
115 ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get(); 117 ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
116 } 118 }
117 119
118 /** 120 /**
121 + * Get draft pages owned by the current user.
122 + * @param int $count
123 + * @param int $page
124 + */
125 + public function getUserDraftPages($count = 20, $page = 0)
126 + {
127 + $user = auth()->user();
128 + return $this->page->where('draft', '=', true)
129 + ->where('created_by', '=', $user->id)
130 + ->orderBy('updated_at', 'desc')
131 + ->skip($count * $page)->take($count)->get();
132 + }
133 +
134 + /**
119 * Updates entity restrictions from a request 135 * Updates entity restrictions from a request
120 * @param $request 136 * @param $request
121 * @param Entity $entity 137 * @param Entity $entity
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 -
4 use Activity; 3 use Activity;
5 use BookStack\Book; 4 use BookStack\Book;
5 +use BookStack\Chapter;
6 use BookStack\Exceptions\NotFoundException; 6 use BookStack\Exceptions\NotFoundException;
7 use Carbon\Carbon; 7 use Carbon\Carbon;
8 use DOMDocument; 8 use DOMDocument;
...@@ -12,6 +12,7 @@ use BookStack\PageRevision; ...@@ -12,6 +12,7 @@ use BookStack\PageRevision;
12 12
13 class PageRepo extends EntityRepo 13 class PageRepo extends EntityRepo
14 { 14 {
15 +
15 protected $pageRevision; 16 protected $pageRevision;
16 17
17 /** 18 /**
...@@ -26,21 +27,27 @@ class PageRepo extends EntityRepo ...@@ -26,21 +27,27 @@ class PageRepo extends EntityRepo
26 27
27 /** 28 /**
28 * Base query for getting pages, Takes restrictions into account. 29 * Base query for getting pages, Takes restrictions into account.
30 + * @param bool $allowDrafts
29 * @return mixed 31 * @return mixed
30 */ 32 */
31 - private function pageQuery() 33 + private function pageQuery($allowDrafts = false)
32 { 34 {
33 - return $this->restrictionService->enforcePageRestrictions($this->page, 'view'); 35 + $query = $this->restrictionService->enforcePageRestrictions($this->page, 'view');
36 + if (!$allowDrafts) {
37 + $query = $query->where('draft', '=', false);
38 + }
39 + return $query;
34 } 40 }
35 41
36 /** 42 /**
37 * Get a page via a specific ID. 43 * Get a page via a specific ID.
38 * @param $id 44 * @param $id
45 + * @param bool $allowDrafts
39 * @return mixed 46 * @return mixed
40 */ 47 */
41 - public function getById($id) 48 + public function getById($id, $allowDrafts = false)
42 { 49 {
43 - return $this->pageQuery()->findOrFail($id); 50 + return $this->pageQuery($allowDrafts)->findOrFail($id);
44 } 51 }
45 52
46 /** 53 /**
...@@ -123,6 +130,47 @@ class PageRepo extends EntityRepo ...@@ -123,6 +130,47 @@ class PageRepo extends EntityRepo
123 return $page; 130 return $page;
124 } 131 }
125 132
133 +
134 + /**
135 + * Publish a draft page to make it a normal page.
136 + * Sets the slug and updates the content.
137 + * @param Page $draftPage
138 + * @param array $input
139 + * @return Page
140 + */
141 + public function publishDraft(Page $draftPage, array $input)
142 + {
143 + $draftPage->fill($input);
144 +
145 + $draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
146 + $draftPage->html = $this->formatHtml($input['html']);
147 + $draftPage->text = strip_tags($draftPage->html);
148 + $draftPage->draft = false;
149 +
150 + $draftPage->save();
151 + return $draftPage;
152 + }
153 +
154 + /**
155 + * Get a new draft page instance.
156 + * @param Book $book
157 + * @param Chapter|null $chapter
158 + * @return static
159 + */
160 + public function getDraftPage(Book $book, $chapter)
161 + {
162 + $page = $this->page->newInstance();
163 + $page->name = 'New Page';
164 + $page->created_by = auth()->user()->id;
165 + $page->updated_by = auth()->user()->id;
166 + $page->draft = true;
167 +
168 + if ($chapter) $page->chapter_id = $chapter->id;
169 +
170 + $book->pages()->save($page);
171 + return $page;
172 + }
173 +
126 /** 174 /**
127 * Formats a page's html to be tagged correctly 175 * Formats a page's html to be tagged correctly
128 * within the system. 176 * within the system.
...@@ -343,6 +391,24 @@ class PageRepo extends EntityRepo ...@@ -343,6 +391,24 @@ class PageRepo extends EntityRepo
343 } 391 }
344 392
345 /** 393 /**
394 + * Update a draft page.
395 + * @param Page $page
396 + * @param array $data
397 + * @return Page
398 + */
399 + public function updateDraftPage(Page $page, $data = [])
400 + {
401 + $page->fill($data);
402 +
403 + if (isset($data['html'])) {
404 + $page->text = strip_tags($data['html']);
405 + }
406 +
407 + $page->save();
408 + return $page;
409 + }
410 +
411 + /**
346 * The base query for getting user update drafts. 412 * The base query for getting user update drafts.
347 * @param Page $page 413 * @param Page $page
348 * @param $userId 414 * @param $userId
......
...@@ -8,15 +8,16 @@ class RestrictionService ...@@ -8,15 +8,16 @@ class RestrictionService
8 protected $userRoles; 8 protected $userRoles;
9 protected $isAdmin; 9 protected $isAdmin;
10 protected $currentAction; 10 protected $currentAction;
11 + protected $currentUser;
11 12
12 /** 13 /**
13 * RestrictionService constructor. 14 * RestrictionService constructor.
14 */ 15 */
15 public function __construct() 16 public function __construct()
16 { 17 {
17 - $user = auth()->user(); 18 + $this->currentUser = auth()->user();
18 - $this->userRoles = $user ? auth()->user()->roles->pluck('id') : []; 19 + $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
19 - $this->isAdmin = $user ? auth()->user()->hasRole('admin') : false; 20 + $this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
20 } 21 }
21 22
22 /** 23 /**
...@@ -48,6 +49,16 @@ class RestrictionService ...@@ -48,6 +49,16 @@ class RestrictionService
48 */ 49 */
49 public function enforcePageRestrictions($query, $action = 'view') 50 public function enforcePageRestrictions($query, $action = 'view')
50 { 51 {
52 + // Prevent drafts being visible to others.
53 + $query = $query->where(function($query) {
54 + $query->where('draft', '=', false);
55 + if ($this->currentUser) {
56 + $query->orWhere(function($query) {
57 + $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
58 + });
59 + }
60 + });
61 +
51 if ($this->isAdmin) return $query; 62 if ($this->isAdmin) return $query;
52 $this->currentAction = $action; 63 $this->currentAction = $action;
53 return $this->pageRestrictionQuery($query); 64 return $this->pageRestrictionQuery($query);
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class ImageEntitiesAndPageDrafts extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('images', function (Blueprint $table) {
16 + $table->string('entity_type', 100);
17 + $table->integer('entity_id');
18 + $table->index(['entity_type', 'entity_id']);
19 + });
20 +
21 + Schema::table('pages', function(Blueprint $table) {
22 + $table->boolean('draft')->default(false);
23 + $table->index('draft');
24 + });
25 + }
26 +
27 + /**
28 + * Reverse the migrations.
29 + *
30 + * @return void
31 + */
32 + public function down()
33 + {
34 + Schema::table('images', function (Blueprint $table) {
35 + $table->dropIndex(['entity_type', 'entity_id']);
36 + $table->dropColumn('entity_type');
37 + $table->dropColumn('entity_id');
38 + });
39 +
40 + Schema::table('pages', function (Blueprint $table) {
41 + $table->dropColumn('draft');
42 + });
43 + }
44 +}
...@@ -4,6 +4,7 @@ module.exports = function (ngApp, events) { ...@@ -4,6 +4,7 @@ module.exports = function (ngApp, events) {
4 4
5 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', 5 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
6 function ($scope, $attrs, $http, $timeout, imageManagerService) { 6 function ($scope, $attrs, $http, $timeout, imageManagerService) {
7 +
7 $scope.images = []; 8 $scope.images = [];
8 $scope.imageType = $attrs.imageType; 9 $scope.imageType = $attrs.imageType;
9 $scope.selectedImage = false; 10 $scope.selectedImage = false;
...@@ -12,6 +13,7 @@ module.exports = function (ngApp, events) { ...@@ -12,6 +13,7 @@ module.exports = function (ngApp, events) {
12 $scope.hasMore = false; 13 $scope.hasMore = false;
13 $scope.imageUpdateSuccess = false; 14 $scope.imageUpdateSuccess = false;
14 $scope.imageDeleteSuccess = false; 15 $scope.imageDeleteSuccess = false;
16 +
15 var page = 0; 17 var page = 0;
16 var previousClickTime = 0; 18 var previousClickTime = 0;
17 var dataLoaded = false; 19 var dataLoaded = false;
...@@ -221,8 +223,13 @@ module.exports = function (ngApp, events) { ...@@ -221,8 +223,13 @@ module.exports = function (ngApp, events) {
221 var pageId = Number($attrs.pageId); 223 var pageId = Number($attrs.pageId);
222 var isEdit = pageId !== 0; 224 var isEdit = pageId !== 0;
223 var autosaveFrequency = 30; // AutoSave interval in seconds. 225 var autosaveFrequency = 30; // AutoSave interval in seconds.
224 - $scope.isDraft = Number($attrs.pageDraft) === 1; 226 + $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
225 - if ($scope.isDraft) $scope.draftText = 'Editing Draft'; 227 + $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
228 + if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
229 + $scope.draftText = 'Editing Draft'
230 + } else {
231 + $scope.draftText = 'Editing Page'
232 + };
226 233
227 var autoSave = false; 234 var autoSave = false;
228 235
...@@ -254,7 +261,7 @@ module.exports = function (ngApp, events) { ...@@ -254,7 +261,7 @@ module.exports = function (ngApp, events) {
254 if (newTitle !== currentContent.title || newHtml !== currentContent.html) { 261 if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
255 currentContent.html = newHtml; 262 currentContent.html = newHtml;
256 currentContent.title = newTitle; 263 currentContent.title = newTitle;
257 - saveDraftUpdate(newTitle, newHtml); 264 + saveDraft(newTitle, newHtml);
258 } 265 }
259 }, 1000 * autosaveFrequency); 266 }, 1000 * autosaveFrequency);
260 } 267 }
...@@ -264,16 +271,22 @@ module.exports = function (ngApp, events) { ...@@ -264,16 +271,22 @@ module.exports = function (ngApp, events) {
264 * @param title 271 * @param title
265 * @param html 272 * @param html
266 */ 273 */
267 - function saveDraftUpdate(title, html) { 274 + function saveDraft(title, html) {
268 $http.put('/ajax/page/' + pageId + '/save-draft', { 275 $http.put('/ajax/page/' + pageId + '/save-draft', {
269 name: title, 276 name: title,
270 html: html 277 html: html
271 }).then((responseData) => { 278 }).then((responseData) => {
272 $scope.draftText = responseData.data.message; 279 $scope.draftText = responseData.data.message;
273 - $scope.isDraft = true; 280 + if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
274 }); 281 });
275 } 282 }
276 283
284 + $scope.forceDraftSave = function() {
285 + var newTitle = $('#name').val();
286 + var newHtml = $scope.editorHtml;
287 + saveDraft(newTitle, newHtml);
288 + };
289 +
277 /** 290 /**
278 * Discard the current draft and grab the current page 291 * Discard the current draft and grab the current page
279 * content from the system via an AJAX request. 292 * content from the system via an AJAX request.
...@@ -281,10 +294,10 @@ module.exports = function (ngApp, events) { ...@@ -281,10 +294,10 @@ module.exports = function (ngApp, events) {
281 $scope.discardDraft = function () { 294 $scope.discardDraft = function () {
282 $http.get('/ajax/page/' + pageId).then((responseData) => { 295 $http.get('/ajax/page/' + pageId).then((responseData) => {
283 if (autoSave) $interval.cancel(autoSave); 296 if (autoSave) $interval.cancel(autoSave);
284 - $scope.draftText = ''; 297 + $scope.draftText = 'Editing Page';
285 - $scope.isDraft = false; 298 + $scope.isUpdateDraft = false;
286 $scope.$broadcast('html-update', responseData.data.html); 299 $scope.$broadcast('html-update', responseData.data.html);
287 - $('#name').val(currentContent.title); 300 + $('#name').val(responseData.data.name);
288 $timeout(() => { 301 $timeout(() => {
289 startAutoSave(); 302 startAutoSave();
290 }, 1000); 303 }, 1000);
......
...@@ -164,7 +164,6 @@ form.search-box { ...@@ -164,7 +164,6 @@ form.search-box {
164 .faded span.faded-text { 164 .faded span.faded-text {
165 display: inline-block; 165 display: inline-block;
166 padding: $-s; 166 padding: $-s;
167 - opacity: 0.5;
168 } 167 }
169 168
170 .faded-small { 169 .faded-small {
......
...@@ -26,6 +26,12 @@ ...@@ -26,6 +26,12 @@
26 .page { 26 .page {
27 border-left: 5px solid $color-page; 27 border-left: 5px solid $color-page;
28 } 28 }
29 + .page.draft {
30 + border-left: 5px solid $color-page-draft;
31 + .text-page {
32 + color: $color-page-draft;
33 + }
34 + }
29 .chapter { 35 .chapter {
30 border-left: 5px solid $color-chapter; 36 border-left: 5px solid $color-chapter;
31 } 37 }
...@@ -182,6 +188,12 @@ ...@@ -182,6 +188,12 @@
182 background-color: rgba($color-page, 0.1); 188 background-color: rgba($color-page, 0.1);
183 } 189 }
184 } 190 }
191 + .list-item-page.draft {
192 + border-left: 5px solid $color-page-draft;
193 + }
194 + .page.draft .page, .list-item-page.draft a.page {
195 + color: $color-page-draft !important;
196 + }
185 .sub-menu { 197 .sub-menu {
186 display: none; 198 display: none;
187 padding-left: 0; 199 padding-left: 0;
...@@ -234,7 +246,6 @@ ...@@ -234,7 +246,6 @@
234 position: absolute; 246 position: absolute;
235 } 247 }
236 248
237 -
238 .activity-list-item { 249 .activity-list-item {
239 padding: $-s 0; 250 padding: $-s 0;
240 color: #888; 251 color: #888;
...@@ -304,6 +315,9 @@ ul.pagination { ...@@ -304,6 +315,9 @@ ul.pagination {
304 font-size: 0.75em; 315 font-size: 0.75em;
305 margin-top: $-xs; 316 margin-top: $-xs;
306 } 317 }
318 + .page.draft .text-page {
319 + color: $color-page-draft;
320 + }
307 } 321 }
308 .entity-list.compact { 322 .entity-list.compact {
309 font-size: 0.6em; 323 font-size: 0.6em;
......
...@@ -45,6 +45,7 @@ $primary-faded: rgba(21, 101, 192, 0.15); ...@@ -45,6 +45,7 @@ $primary-faded: rgba(21, 101, 192, 0.15);
45 $color-book: #009688; 45 $color-book: #009688;
46 $color-chapter: #ef7c3c; 46 $color-chapter: #ef7c3c;
47 $color-page: $primary; 47 $color-page: $primary;
48 +$color-page-draft: #9A60DA;
48 49
49 // Text colours 50 // Text colours
50 $text-dark: #444; 51 $text-dark: #444;
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 <div class="row"> 7 <div class="row">
8 <div class="col-md-4 faded"> 8 <div class="col-md-4 faded">
9 <div class="breadcrumbs"> 9 <div class="breadcrumbs">
10 - <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->name }}</a> 10 + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
11 </div> 11 </div>
12 </div> 12 </div>
13 <div class="col-md-8 faded"> 13 <div class="col-md-8 faded">
......
...@@ -23,6 +23,12 @@ ...@@ -23,6 +23,12 @@
23 <div class="row"> 23 <div class="row">
24 24
25 <div class="col-sm-4"> 25 <div class="col-sm-4">
26 + <div id="recent-drafts">
27 + @if(count($draftPages) > 0)
28 + <h3>My Recent Drafts</h3>
29 + @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
30 + @endif
31 + </div>
26 @if($signedIn) 32 @if($signedIn)
27 <h3>My Recently Viewed</h3> 33 <h3>My Recently Viewed</h3>
28 @else 34 @else
......
1 @extends('base') 1 @extends('base')
2 2
3 @section('head') 3 @section('head')
4 - <script src="/libs/tinymce/tinymce.min.js?ver=4.3.2"></script> 4 + <script src="/libs/tinymce/tinymce.min.js?ver=4.3.7"></script>
5 @stop 5 @stop
6 6
7 @section('body-class', 'flexbox') 7 @section('body-class', 'flexbox')
...@@ -9,11 +9,8 @@ ...@@ -9,11 +9,8 @@
9 @section('content') 9 @section('content')
10 10
11 <div class="flex-fill flex"> 11 <div class="flex-fill flex">
12 - <form action="{{$book->getUrl() . '/page'}}" method="POST" class="flex flex-fill"> 12 + <form action="{{$book->getUrl() . '/page/' . $draft->id}}" method="POST" class="flex flex-fill">
13 - @include('pages/form') 13 + @include('pages/form', ['model' => $draft])
14 - @if($chapter)
15 - <input type="hidden" name="chapter" value="{{$chapter->id}}">
16 - @endif
17 </form> 14 </form>
18 </div> 15 </div>
19 @include('partials/image-manager', ['imageType' => 'gallery']) 16 @include('partials/image-manager', ['imageType' => 'gallery'])
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
3 @section('content') 3 @section('content')
4 4
5 <div class="container small" ng-non-bindable> 5 <div class="container small" ng-non-bindable>
6 - <h1>Delete Page</h1> 6 + <h1>Delete {{ $page->draft ? 'Draft' : '' }} Page</h1>
7 - <p class="text-neg">Are you sure you want to delete this page?</p> 7 + <p class="text-neg">Are you sure you want to delete this {{ $page->draft ? 'draft' : '' }} page?</p>
8 8
9 <form action="{{$page->getUrl()}}" method="POST"> 9 <form action="{{$page->getUrl()}}" method="POST">
10 {!! csrf_field() !!} 10 {!! csrf_field() !!}
......
1 @extends('base') 1 @extends('base')
2 2
3 @section('head') 3 @section('head')
4 - <script src="/libs/tinymce/tinymce.min.js?ver=4.3.2"></script> 4 + <script src="/libs/tinymce/tinymce.min.js?ver=4.3.7"></script>
5 @stop 5 @stop
6 6
7 @section('body-class', 'flexbox') 7 @section('body-class', 'flexbox')
......
1 1
2 - 2 +<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 }}">
3 -
4 -<div class="page-editor flex-fill flex" ng-controller="PageEditController" page-id="{{ $model->id or 0 }}" page-draft="{{ $page->isDraft or 0 }}">
5 3
6 {{ csrf_field() }} 4 {{ csrf_field() }}
7 <div class="faded-small toolbar"> 5 <div class="faded-small toolbar">
...@@ -14,11 +12,23 @@ ...@@ -14,11 +12,23 @@
14 </div> 12 </div>
15 </div> 13 </div>
16 <div class="col-sm-4 faded text-center"> 14 <div class="col-sm-4 faded text-center">
17 - <span class="faded-text" ng-bind="draftText"></span> 15 +
16 + <div dropdown class="dropdown-container">
17 + <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>
18 + <ul>
19 + <li>
20 + <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
21 + </li>
22 + <li ng-if="isNewPageDraft">
23 + <a href="{{$model->getUrl()}}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
24 + </li>
25 + </ul>
26 + </div>
18 </div> 27 </div>
19 <div class="col-sm-4 faded"> 28 <div class="col-sm-4 faded">
20 <div class="action-buttons" ng-cloak> 29 <div class="action-buttons" ng-cloak>
21 - <button type="button" ng-if="isDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button> 30 +
31 + <button type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button>
22 <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button> 32 <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
23 </div> 33 </div>
24 </div> 34 </div>
......
1 -<div class="page"> 1 +<div class="page {{$page->draft ? 'draft' : ''}}">
2 <h3> 2 <h3>
3 <a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a> 3 <a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a>
4 </h3> 4 </h3>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 6
7 7
8 @foreach($sidebarTree as $bookChild) 8 @foreach($sidebarTree as $bookChild)
9 - <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }}"> 9 + <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
10 <a href="{{$bookChild->getUrl()}}" class="{{ $bookChild->getClassName() }} {{ $current->matches($bookChild)? 'selected' : '' }}"> 10 <a href="{{$bookChild->getUrl()}}" class="{{ $bookChild->getClassName() }} {{ $current->matches($bookChild)? 'selected' : '' }}">
11 @if($bookChild->isA('chapter'))<i class="zmdi zmdi-collection-bookmark"></i>@else <i class="zmdi zmdi-file-text"></i>@endif{{ $bookChild->name }} 11 @if($bookChild->isA('chapter'))<i class="zmdi zmdi-collection-bookmark"></i>@else <i class="zmdi zmdi-file-text"></i>@endif{{ $bookChild->name }}
12 </a> 12 </a>
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
17 </p> 17 </p>
18 <ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif"> 18 <ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">
19 @foreach($bookChild->pages as $childPage) 19 @foreach($bookChild->pages as $childPage)
20 - <li class="list-item-page"> 20 + <li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}">
21 <a href="{{$childPage->getUrl()}}" class="page {{ $current->matches($childPage)? 'selected' : '' }}"> 21 <a href="{{$childPage->getUrl()}}" class="page {{ $current->matches($childPage)? 'selected' : '' }}">
22 <i class="zmdi zmdi-file-text"></i> {{ $childPage->name }} 22 <i class="zmdi zmdi-file-text"></i> {{ $childPage->name }}
23 </a> 23 </a>
......
...@@ -88,8 +88,11 @@ class EntityTest extends TestCase ...@@ -88,8 +88,11 @@ class EntityTest extends TestCase
88 $this->asAdmin() 88 $this->asAdmin()
89 // Navigate to page create form 89 // Navigate to page create form
90 ->visit($chapter->getUrl()) 90 ->visit($chapter->getUrl())
91 - ->click('New Page') 91 + ->click('New Page');
92 - ->seePageIs($chapter->getUrl() . '/create-page') 92 +
93 + $draftPage = \BookStack\Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
94 +
95 + $this->seePageIs($draftPage->getUrl())
93 // Fill out form 96 // Fill out form
94 ->type($page->name, '#name') 97 ->type($page->name, '#name')
95 ->type($page->html, '#html') 98 ->type($page->html, '#html')
......
1 <?php 1 <?php
2 2
3 3
4 -class PageUpdateDraftTest extends TestCase 4 +class PageDraftTest extends TestCase
5 { 5 {
6 protected $page; 6 protected $page;
7 protected $pageRepo; 7 protected $pageRepo;
...@@ -59,4 +59,33 @@ class PageUpdateDraftTest extends TestCase ...@@ -59,4 +59,33 @@ class PageUpdateDraftTest extends TestCase
59 ->see('Admin has started editing this page'); 59 ->see('Admin has started editing this page');
60 } 60 }
61 61
62 + public function test_draft_pages_show_on_homepage()
63 + {
64 + $book = \BookStack\Book::first();
65 + $this->asAdmin()->visit('/')
66 + ->dontSeeInElement('#recent-drafts', 'New Page')
67 + ->visit($book->getUrl() . '/page/create')
68 + ->visit('/')
69 + ->seeInElement('#recent-drafts', 'New Page');
70 + }
71 +
72 + public function test_draft_pages_not_visible_by_others()
73 + {
74 + $book = \BookStack\Book::first();
75 + $chapter = $book->chapters->first();
76 + $newUser = $this->getNewUser();
77 +
78 + $this->actingAs($newUser)->visit('/')
79 + ->visit($book->getUrl() . '/page/create')
80 + ->visit($chapter->getUrl() . '/create-page')
81 + ->visit($book->getUrl())
82 + ->seeInElement('.page-list', 'New Page');
83 +
84 + $this->asAdmin()
85 + ->visit($book->getUrl())
86 + ->dontSeeInElement('.page-list', 'New Page')
87 + ->visit($chapter->getUrl())
88 + ->dontSeeInElement('.page-list', 'New Page');
89 + }
90 +
62 } 91 }
......
...@@ -392,14 +392,28 @@ class RolesTest extends TestCase ...@@ -392,14 +392,28 @@ class RolesTest extends TestCase
392 392
393 $baseUrl = $ownBook->getUrl() . '/page'; 393 $baseUrl = $ownBook->getUrl() . '/page';
394 394
395 - $this->checkAccessPermission('page-create-own', [ 395 + $createUrl = $baseUrl . '/create';
396 - $baseUrl . '/create', 396 + $createUrlChapter = $ownChapter->getUrl() . '/create-page';
397 - $ownChapter->getUrl() . '/create-page' 397 + $accessUrls = [$createUrl, $createUrlChapter];
398 - ], [ 398 +
399 + foreach ($accessUrls as $url) {
400 + $this->actingAs($this->user)->visit('/')->visit($url)
401 + ->seePageIs('/');
402 + }
403 +
404 + $this->checkAccessPermission('page-create-own', [], [
399 $ownBook->getUrl() => 'New Page', 405 $ownBook->getUrl() => 'New Page',
400 $ownChapter->getUrl() => 'New Page' 406 $ownChapter->getUrl() => 'New Page'
401 ]); 407 ]);
402 408
409 + $this->giveUserPermissions($this->user, ['page-create-own']);
410 +
411 + foreach ($accessUrls as $index => $url) {
412 + $this->actingAs($this->user)->visit('/')->visit($url);
413 + $expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
414 + $this->seePageIs($expectedUrl);
415 + }
416 +
403 $this->visit($baseUrl . '/create') 417 $this->visit($baseUrl . '/create')
404 ->type('test page', 'name') 418 ->type('test page', 'name')
405 ->type('page desc', 'html') 419 ->type('page desc', 'html')
...@@ -421,14 +435,29 @@ class RolesTest extends TestCase ...@@ -421,14 +435,29 @@ class RolesTest extends TestCase
421 $book = \BookStack\Book::take(1)->get()->first(); 435 $book = \BookStack\Book::take(1)->get()->first();
422 $chapter = \BookStack\Chapter::take(1)->get()->first(); 436 $chapter = \BookStack\Chapter::take(1)->get()->first();
423 $baseUrl = $book->getUrl() . '/page'; 437 $baseUrl = $book->getUrl() . '/page';
424 - $this->checkAccessPermission('page-create-all', [ 438 + $createUrl = $baseUrl . '/create';
425 - $baseUrl . '/create', 439 +
426 - $chapter->getUrl() . '/create-page' 440 + $createUrlChapter = $chapter->getUrl() . '/create-page';
427 - ], [ 441 + $accessUrls = [$createUrl, $createUrlChapter];
442 +
443 + foreach ($accessUrls as $url) {
444 + $this->actingAs($this->user)->visit('/')->visit($url)
445 + ->seePageIs('/');
446 + }
447 +
448 + $this->checkAccessPermission('page-create-all', [], [
428 $book->getUrl() => 'New Page', 449 $book->getUrl() => 'New Page',
429 $chapter->getUrl() => 'New Page' 450 $chapter->getUrl() => 'New Page'
430 ]); 451 ]);
431 452
453 + $this->giveUserPermissions($this->user, ['page-create-all']);
454 +
455 + foreach ($accessUrls as $index => $url) {
456 + $this->actingAs($this->user)->visit('/')->visit($url);
457 + $expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
458 + $this->seePageIs($expectedUrl);
459 + }
460 +
432 $this->visit($baseUrl . '/create') 461 $this->visit($baseUrl . '/create')
433 ->type('test page', 'name') 462 ->type('test page', 'name')
434 ->type('page desc', 'html') 463 ->type('page desc', 'html')
......