]> BookStack Code Mirror - bookstack/commitdiff
Finished off page move functionality
authorDan Brown <redacted>
Sun, 12 Jun 2016 11:14:14 +0000 (12:14 +0100)
committerDan Brown <redacted>
Sun, 12 Jun 2016 11:14:14 +0000 (12:14 +0100)
15 files changed:
app/Http/Controllers/PageController.php
app/Http/Controllers/SearchController.php
app/Http/routes.php
app/Repos/PageRepo.php
app/Services/ViewService.php
resources/assets/js/directives.js
resources/assets/sass/_forms.scss
resources/assets/sass/styles.scss
resources/lang/en/activities.php
resources/views/books/list-item.blade.php
resources/views/chapters/list-item.blade.php
resources/views/pages/list-item.blade.php
resources/views/pages/move.blade.php
resources/views/pages/show.blade.php
resources/views/partials/custom-styles.blade.php

index 52de19b5b0faa6a0d70b7e6248cec57e745701ac..373e49de762a2a113793cc20a96b9c195ea0d9cc 100644 (file)
@@ -221,8 +221,8 @@ class PageController extends Controller
         $updateTime = $draft->updated_at->timestamp;
         $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
         return response()->json([
-            'status' => 'success',
-            'message' => 'Draft saved at ',
+            'status'    => 'success',
+            'message'   => 'Draft saved at ',
             'timestamp' => $utcUpdateTimestamp
         ]);
     }
@@ -468,6 +468,41 @@ class PageController extends Controller
         ]);
     }
 
+    public function move($bookSlug, $pageSlug, Request $request)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $this->checkOwnablePermission('page-update', $page);
+
+        $entitySelection = $request->get('entity_selection', null);
+        if ($entitySelection === null || $entitySelection === '') {
+            return redirect($page->getUrl());
+        }
+
+        $stringExploded = explode(':', $entitySelection);
+        $entityType = $stringExploded[0];
+        $entityId = intval($stringExploded[1]);
+
+        $parent = false;
+
+        if ($entityType == 'chapter') {
+            $parent = $this->chapterRepo->getById($entityId);
+        } else if ($entityType == 'book') {
+            $parent = $this->bookRepo->getById($entityId);
+        }
+
+        if ($parent === false || $parent === null) {
+            session()->flash('The selected Book or Chapter was not found');
+            return redirect()->back();
+        }
+
+        $this->pageRepo->changePageParent($page, $parent);
+        Activity::add($page, 'page_move', $page->book->id);
+        session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
+
+        return redirect($page->getUrl());
+    }
+
     /**
      * Set the permissions for this page.
      * @param $bookSlug
index e198dc767913b9ac2f3ae5ffb7b8bdae00b5056f..7c7d7b2545dfaafe64c585eff684dad8f25482fc 100644 (file)
@@ -2,10 +2,10 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Services\ViewService;
 use Illuminate\Http\Request;
 
 use BookStack\Http\Requests;
-use BookStack\Http\Controllers\Controller;
 use BookStack\Repos\BookRepo;
 use BookStack\Repos\ChapterRepo;
 use BookStack\Repos\PageRepo;
@@ -15,18 +15,21 @@ class SearchController extends Controller
     protected $pageRepo;
     protected $bookRepo;
     protected $chapterRepo;
+    protected $viewService;
 
     /**
      * SearchController constructor.
-     * @param $pageRepo
-     * @param $bookRepo
-     * @param $chapterRepo
+     * @param PageRepo $pageRepo
+     * @param BookRepo $bookRepo
+     * @param ChapterRepo $chapterRepo
+     * @param ViewService $viewService
      */
-    public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
+    public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService)
     {
         $this->pageRepo = $pageRepo;
         $this->bookRepo = $bookRepo;
         $this->chapterRepo = $chapterRepo;
+        $this->viewService = $viewService;
         parent::__construct();
     }
 
@@ -48,9 +51,9 @@ class SearchController extends Controller
         $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
         $this->setPageTitle('Search For ' . $searchTerm);
         return view('search/all', [
-            'pages' => $pages,
-            'books' => $books,
-            'chapters' => $chapters,
+            'pages'      => $pages,
+            'books'      => $books,
+            'chapters'   => $chapters,
             'searchTerm' => $searchTerm
         ]);
     }
@@ -69,8 +72,8 @@ class SearchController extends Controller
         $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
         $this->setPageTitle('Page Search For ' . $searchTerm);
         return view('search/entity-search-list', [
-            'entities' => $pages,
-            'title' => 'Page Search Results',
+            'entities'   => $pages,
+            'title'      => 'Page Search Results',
             'searchTerm' => $searchTerm
         ]);
     }
@@ -89,8 +92,8 @@ class SearchController extends Controller
         $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
         $this->setPageTitle('Chapter Search For ' . $searchTerm);
         return view('search/entity-search-list', [
-            'entities' => $chapters,
-            'title' => 'Chapter Search Results',
+            'entities'   => $chapters,
+            'title'      => 'Chapter Search Results',
             'searchTerm' => $searchTerm
         ]);
     }
@@ -109,8 +112,8 @@ class SearchController extends Controller
         $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
         $this->setPageTitle('Book Search For ' . $searchTerm);
         return view('search/entity-search-list', [
-            'entities' => $books,
-            'title' => 'Book Search Results',
+            'entities'   => $books,
+            'title'      => 'Book Search Results',
             'searchTerm' => $searchTerm
         ]);
     }
@@ -134,4 +137,35 @@ class SearchController extends Controller
         return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
     }
 
+
+    /**
+     * Search for a list of entities and return a partial HTML response of matching entities.
+     * Returns the most popular entities if no search is provided.
+     * @param Request $request
+     * @return mixed
+     */
+    public function searchEntitiesAjax(Request $request)
+    {
+        $entities = collect();
+        $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
+        $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
+
+        // Search for entities otherwise show most popular
+        if ($searchTerm !== false) {
+            if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items());
+            if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items());
+            if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items());
+            $entities = $entities->sortByDesc('title_relevance');
+        } else {
+            $entityNames = $entityTypes->map(function ($type) {
+                return 'BookStack\\' . ucfirst($type);
+            })->toArray();
+            $entities = $this->viewService->getPopular(20, 0, $entityNames);
+        }
+
+        return view('partials/entity-list', ['entities' => $entities]);
+    }
+
 }
+
+
index 90bcd593f800028ec9d13b308f29dd1d14aa3031..d7c090953f2c741997a2c75eb7cfc45ad32ca012 100644 (file)
@@ -35,6 +35,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
         Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
         Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
+        Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
         Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
         Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
         Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
@@ -94,6 +95,8 @@ Route::group(['middleware' => 'auth'], function () {
         Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
     });
 
+    Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
+
     // Links
     Route::get('/link/{id}', 'PageController@redirectFromLink');
 
index 504c3fa3b6f549e0facbaa991e863e80c3a1bcdb..de050e1c7cb8ad245157f81316c6305c2fdff87a 100644 (file)
@@ -3,6 +3,7 @@
 use Activity;
 use BookStack\Book;
 use BookStack\Chapter;
+use BookStack\Entity;
 use BookStack\Exceptions\NotFoundException;
 use Carbon\Carbon;
 use DOMDocument;
@@ -572,6 +573,22 @@ class PageRepo extends EntityRepo
         return $page;
     }
 
+
+    /**
+     * Change the page's parent to the given entity.
+     * @param Page $page
+     * @param Entity $parent
+     */
+    public function changePageParent(Page $page, Entity $parent)
+    {
+        $book = $parent->isA('book') ? $parent : $parent->book;
+        $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
+        $page->save();
+        $page = $this->changeBook($book->id, $page);
+        $page->load('book');
+        $this->permissionService->buildJointPermissionsForEntity($book);
+    }
+
     /**
      * Gets a suitable slug for the resource
      * @param            $name
index 849a164cf409995a596dd7113d34bebb47d50601..aac9831f74cac2ffee9c84c19f6e94e18934bb34 100644 (file)
@@ -50,7 +50,7 @@ class ViewService
      * Get the entities with the most views.
      * @param int $count
      * @param int $page
-     * @param bool|false $filterModel
+     * @param bool|false|array $filterModel
      */
     public function getPopular($count = 10, $page = 0, $filterModel = false)
     {
@@ -60,7 +60,11 @@ class ViewService
             ->groupBy('viewable_id', 'viewable_type')
             ->orderBy('view_count', 'desc');
 
-        if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
+        if ($filterModel && is_array($filterModel)) {
+            $query->whereIn('viewable_type', $filterModel);
+        } else if ($filterModel) {
+            $query->where('viewable_type', '=', get_class($filterModel));
+        };
 
         return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
     }
index 54df2d2bfea8a5e7d777c561fc1ba12f09193e84..f1fefd241318367ee9fed813a8aeb9f3a99ee2f7 100644 (file)
@@ -584,12 +584,62 @@ module.exports = function (ngApp, events) {
     }]);
 
 
-    ngApp.directive('entitySelector', ['$http', function ($http) {
+    ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
         return {
             restrict: 'A',
+            scope: true,
             link: function (scope, element, attrs) {
                 scope.loading = true;
-                
+                scope.entityResults = false;
+                scope.search = '';
+
+                // Add input for forms
+                const input = element.find('[entity-selector-input]').first();
+
+                // Listen to entity item clicks
+                element.on('click', '.entity-list a', function(event) {
+                    event.preventDefault();
+                    event.stopPropagation();
+                    let item = $(this).closest('[data-entity-type]');
+                    itemSelect(item);
+                });
+                element.on('click', '[data-entity-type]', function(event) {
+                    itemSelect($(this));
+                });
+
+                // Select entity action
+                function itemSelect(item) {
+                    let entityType = item.attr('data-entity-type');
+                    let entityId = item.attr('data-entity-id');
+                    let isSelected = !item.hasClass('selected');
+                    element.find('.selected').removeClass('selected').removeClass('primary-background');
+                    if (isSelected) item.addClass('selected').addClass('primary-background');
+                    let newVal = isSelected ? `${entityType}:${entityId}` : '';
+                    input.val(newVal);
+                }
+
+                // Get search url with correct types
+                function getSearchUrl() {
+                    let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter');
+                    return `/ajax/search/entities?types=${types}`;
+                }
+
+                // Get initial contents
+                $http.get(getSearchUrl()).then(resp => {
+                    scope.entityResults = $sce.trustAsHtml(resp.data);
+                    scope.loading = false;
+                });
+
+                // Search when typing
+                scope.searchEntities = function() {
+                    scope.loading = true;
+                    input.val('');
+                    let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search);
+                    $http.get(url).then(resp => {
+                        scope.entityResults = $sce.trustAsHtml(resp.data);
+                        scope.loading = false;
+                    });
+                };
             }
         };
     }]);
index da015ec7cfed97fa80bc10bc6db51e2edc099fb1..4e643dcdaa5ab9de5ec6be9ec8ef4c6881d34c07 100644 (file)
@@ -20,6 +20,9 @@
   &.disabled, &[disabled] {
     background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
   }
+  &:focus {
+    outline: 0;
+  }
 }
 
 #html-editor {
index 0a7da179b09c58d3ec01ca65db36050949a9bca4..770d5eeb45fc4eb07f02c6057968249674434db5 100644 (file)
@@ -207,3 +207,59 @@ $btt-size: 40px;
     color: #EEE;
   }
 }
+
+.entity-selector {
+  border: 1px solid #DDD;
+  border-radius: 3px;
+  overflow: hidden;
+  font-size: 0.8em;
+  input[type="text"] {
+    width: 100%;
+    display: block;
+    border-radius: 0;
+    border: 0;
+    border-bottom: 1px solid #DDD;
+    font-size: 16px;
+    padding: $-s $-m;
+  }
+  .entity-list {
+    overflow-y: scroll;
+    height: 400px;
+    background-color: #EEEEEE;
+  }
+  .loading {
+    height: 400px;
+    padding-top: $-l;
+  }
+  .entity-list > p {
+    text-align: center;
+    padding-top: $-l;
+    font-size: 1.333em;
+  }
+  .entity-list > div {
+    padding-left: $-m;
+    padding-right: $-m;
+    background-color: #FFF;
+    transition: all ease-in-out 120ms;
+    cursor: pointer;
+  }
+}
+
+.entity-list-item.selected {
+  h3, i, p ,a {
+    color: #EEE;
+  }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
index 8356d8302d950180a06d32de43ce95a680b80752..c59513aa95a57901d8a26c06ce978e0969be7927 100644 (file)
@@ -16,6 +16,7 @@ return [
     'page_delete_notification'    => 'Page Successfully Deleted',
     'page_restore'                => 'restored page',
     'page_restore_notification'   => 'Page Successfully Restored',
+    'page_move'                   => 'moved page',
 
     // Chapters
     'chapter_create'              => 'created chapter',
index d3e0ef56bfba30f2f340152ba75f3ed964c664f1..945eb901563c1a9b64a8e02afe79ba4cf5278a30 100644 (file)
@@ -1,4 +1,4 @@
-<div class="book" data-entity-type="book" data-entity-id="{{$book->id}}">
+<div class="book entity-list-item"  data-entity-type="book" data-entity-id="{{$book->id}}">
     <h3 class="text-book"><a class="text-book" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></h3>
     @if(isset($book->searchSnippet))
         <p class="text-muted">{!! $book->searchSnippet !!}</p>
index 71225e9879c34f5bb627c8ed50c1dacdaba95025..f66c3781f9aaa7a350d6e702bc80bf890c7e18c8 100644 (file)
@@ -1,4 +1,4 @@
-<div class="chapter" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
+<div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
     <h3>
         <a href="{{ $chapter->getUrl() }}" class="text-chapter">
             <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }}
index 100c1de48a6095f07405113e942b0c64f4a90edd..a95870db044725453b3404daf20d94e6796b08ea 100644 (file)
@@ -1,4 +1,4 @@
-<div class="page {{$page->draft ? 'draft' : ''}}" data-entity-type="page" data-entity-id="{{$page->id}}">
+<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
     <h3>
         <a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a>
     </h3>
index 2ccbdb438995151426a0722cb411fd91a9c6bf2e..2c9437e770f1c27d860e2ecf406efbbc746b7312 100644 (file)
     <div class="container">
         <h1>Move Page <small class="subheader">{{$page->name}}</small></h1>
 
-        <div class="bordered" ng-cloak entity-selector>
-            <input type="text" placeholder="Search">
-            <div class="text-center" ng-if="loading">@include('partials/loading-icon')</div>
-        </div>
+        <form action="{{ $page->getUrl() }}/move" method="POST">
+            {!! csrf_field() !!}
+            <input type="hidden" name="_method" value="PUT">
+
+            <div class="form-group">
+                <div entity-selector class="entity-selector large" entity-types="book,chapter">
+                    <input type="hidden" entity-selector-input name="entity_selection">
+                    <input type="text" placeholder="Search" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
+                    <div class="text-center loading" ng-show="loading">@include('partials/loading-icon')</div>
+                    <div ng-show="!loading" ng-bind-html="entityResults"></div>
+                </div>
+            </div>
+
+            <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a>
+            <button type="submit" class="button pos">Move Page</button>
+        </form>
     </div>
 
 @stop
index 8b9b6349280fe7a471ee887ebc2a105c1522f148..f12ba58c61321a9f69ec444b478d1a56a4bd2a4f 100644 (file)
                             </ul>
                         </span>
                         @if(userCan('page-update', $page))
-                            <a href="{{$page->getUrl()}}/revisions" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
                             <a href="{{$page->getUrl()}}/edit" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
-                            <a href="{{$page->getUrl()}}/move" class="text-primary text-button" ><i class="zmdi zmdi-folder"></i>Move</a>
                         @endif
-                        @if(userCan('restrictions-manage', $page))
-                            <a href="{{$page->getUrl()}}/permissions" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Permissions</a>
-                        @endif
-                        @if(userCan('page-delete', $page))
-                            <a href="{{$page->getUrl()}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
+                        @if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
+                            <div dropdown class="dropdown-container">
+                                <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
+                                <ul>
+                                    @if(userCan('page-update', $page))
+                                        <li><a href="{{$page->getUrl()}}/move" class="text-primary" ><i class="zmdi zmdi-folder"></i>Move</a></li>
+                                        <li><a href="{{$page->getUrl()}}/revisions" class="text-primary"><i class="zmdi zmdi-replay"></i>Revisions</a></li>
+                                    @endif
+                                    @if(userCan('restrictions-manage', $page))
+                                        <li><a href="{{$page->getUrl()}}/permissions" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li>
+                                    @endif
+                                    @if(userCan('page-delete', $page))
+                                        <li><a href="{{$page->getUrl()}}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
+                                    @endif
+                                </ul>
+                            </div>
                         @endif
+
                     </div>
                 </div>
             </div>
index 4d3d0a6ef7cdcafb8c3783346228f80a8584beae..e6f8f7fc204e101b6cbafc29da485e1bc315ad92 100644 (file)
@@ -1,7 +1,7 @@
 @if(Setting::get('app-color'))
     <style>
         header, #back-to-top, .primary-background {
-            background-color: {{ Setting::get('app-color') }};
+            background-color: {{ Setting::get('app-color') }} !important;
         }
         .faded-small, .primary-background-light {
             background-color: {{ Setting::get('app-color-light') }};