Added new page drafts and started image entity attaching
Closes #80.
Showing
26 changed files
with
403 additions
and
84 deletions
| ... | @@ -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> <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') | ... | ... |
-
Please register or sign in to post a comment