]> BookStack Code Mirror - bookstack/commitdiff
Added ability to copy a page
authorDan Brown <redacted>
Sat, 14 Apr 2018 17:00:16 +0000 (18:00 +0100)
committerDan Brown <redacted>
Sat, 14 Apr 2018 17:00:16 +0000 (18:00 +0100)
In 'More' menu alongside move.
Allows you to move if you have permission to create within the new
target parent.
Closes #673

17 files changed:
app/Entity.php
app/Http/Controllers/PageController.php
app/Http/Controllers/SearchController.php
app/Repos/EntityRepo.php
app/Services/PermissionService.php
app/Services/SearchService.php
app/Services/ViewService.php
package-lock.json
resources/assets/js/components/entity-selector.js
resources/lang/en/common.php
resources/lang/en/entities.php
resources/views/books/form.blade.php
resources/views/components/entity-selector.blade.php
resources/views/pages/copy.blade.php [new file with mode: 0644]
resources/views/pages/show.blade.php
routes/web.php
tests/Entity/SortTest.php

index 67edec4e089eefe3d4eb69472da891cde409ed81..5d4449f2bd7817e38db5eb0284baa781c7e0858e 100644 (file)
@@ -197,8 +197,8 @@ class Entity extends Ownable
      * @param $path
      * @return string
      */
      * @param $path
      * @return string
      */
-    public function getUrl($path)
+    public function getUrl($path = '/')
     {
     {
-        return '/';
+        return $path;
     }
 }
     }
 }
index 9cc73ae154536abc23d0121bf3e30539ef2eb418..f42cf63ec893f4a9c5aadf0c75c1938cf01eebfb 100644 (file)
@@ -592,12 +592,70 @@ class PageController extends Controller
         return redirect($page->getUrl());
     }
 
         return redirect($page->getUrl());
     }
 
+    /**
+     * Show the view to copy a page.
+     * @param string $bookSlug
+     * @param string $pageSlug
+     * @return mixed
+     * @throws NotFoundException
+     */
+    public function showCopy($bookSlug, $pageSlug)
+    {
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+        $this->checkOwnablePermission('page-update', $page);
+        session()->flashInput(['name' => $page->name]);
+        return view('pages/copy', [
+            'book' => $page->book,
+            'page' => $page
+        ]);
+    }
+
+    /**
+     * Create a copy of a page within the requested target destination.
+     * @param string $bookSlug
+     * @param string $pageSlug
+     * @param Request $request
+     * @return mixed
+     * @throws NotFoundException
+     */
+    public function copy($bookSlug, $pageSlug, Request $request)
+    {
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+        $this->checkOwnablePermission('page-update', $page);
+
+        $entitySelection = $request->get('entity_selection', null);
+        if ($entitySelection === null || $entitySelection === '') {
+            $parent = $page->chapter ? $page->chapter : $page->book;
+        } else {
+            $stringExploded = explode(':', $entitySelection);
+            $entityType = $stringExploded[0];
+            $entityId = intval($stringExploded[1]);
+
+            try {
+                $parent = $this->entityRepo->getById($entityType, $entityId);
+            } catch (\Exception $e) {
+                session()->flash(trans('entities.selected_book_chapter_not_found'));
+                return redirect()->back();
+            }
+        }
+
+        $this->checkOwnablePermission('page-create', $parent);
+
+        $pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
+
+        Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
+        session()->flash('success', trans('entities.pages_copy_success'));
+
+        return redirect($pageCopy->getUrl());
+    }
+
     /**
      * Set the permissions for this page.
      * @param string $bookSlug
      * @param string $pageSlug
      * @param Request $request
      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     /**
      * Set the permissions for this page.
      * @param string $bookSlug
      * @param string $pageSlug
      * @param Request $request
      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @throws NotFoundException
      */
     public function restrict($bookSlug, $pageSlug, Request $request)
     {
      */
     public function restrict($bookSlug, $pageSlug, Request $request)
     {
index ddc92f7055cb97570cfca5ce74b0398ac461d18a..49f9885adb24c1bb438f5b95f6cd20fd0f7937a7 100644 (file)
@@ -89,16 +89,17 @@ class SearchController extends Controller
     {
         $entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
         $searchTerm =  $request->get('term', false);
     {
         $entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
         $searchTerm =  $request->get('term', false);
+        $permission = $request->get('permission', 'view');
 
         // Search for entities otherwise show most popular
         if ($searchTerm !== false) {
             $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
 
         // Search for entities otherwise show most popular
         if ($searchTerm !== false) {
             $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
-            $entities = $this->searchService->searchEntities($searchTerm)['results'];
+            $entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
         } else {
             $entityNames = $entityTypes->map(function ($type) {
                 return 'BookStack\\' . ucfirst($type);
             })->toArray();
         } else {
             $entityNames = $entityTypes->map(function ($type) {
                 return 'BookStack\\' . ucfirst($type);
             })->toArray();
-            $entities = $this->viewService->getPopular(20, 0, $entityNames);
+            $entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
         }
 
         return view('search/entity-ajax-list', ['entities' => $entities]);
         }
 
         return view('search/entity-ajax-list', ['entities' => $entities]);
index 14f9d8d0eaf8c2d7a56fbed7280bdf3b79e4ec1e..bdd1e37b10ddfd58c699f6d345a2e53acb58850c 100644 (file)
@@ -593,6 +593,30 @@ class EntityRepo
         return $slug;
     }
 
         return $slug;
     }
 
+    /**
+     * Get a new draft page instance.
+     * @param Book $book
+     * @param Chapter|bool $chapter
+     * @return Page
+     */
+    public function getDraftPage(Book $book, $chapter = false)
+    {
+        $page = $this->page->newInstance();
+        $page->name = trans('entities.pages_initial_name');
+        $page->created_by = user()->id;
+        $page->updated_by = user()->id;
+        $page->draft = true;
+
+        if ($chapter) {
+            $page->chapter_id = $chapter->id;
+        }
+
+        $book->pages()->save($page);
+        $page = $this->page->find($page->id);
+        $this->permissionService->buildJointPermissionsForEntity($page);
+        return $page;
+    }
+
     /**
      * Publish a draft page to make it a normal page.
      * Sets the slug and updates the content.
     /**
      * Publish a draft page to make it a normal page.
      * Sets the slug and updates the content.
@@ -621,6 +645,43 @@ class EntityRepo
         return $draftPage;
     }
 
         return $draftPage;
     }
 
+    /**
+     * Create a copy of a page in a new location with a new name.
+     * @param Page $page
+     * @param Entity $newParent
+     * @param string $newName
+     * @return Page
+     */
+    public function copyPage(Page $page, Entity $newParent, $newName = '')
+    {
+        $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
+        $newChapter = $newParent->isA('chapter') ? $newParent : null;
+        $copyPage = $this->getDraftPage($newBook, $newChapter);
+        $pageData = $page->getAttributes();
+
+        // Update name
+        if (!empty($newName)) {
+            $pageData['name'] = $newName;
+        }
+
+        // Copy tags from previous page if set
+        if ($page->tags) {
+            $pageData['tags'] = [];
+            foreach ($page->tags as $tag) {
+                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+            }
+        }
+
+        // Set priority
+        if ($newParent->isA('chapter')) {
+            $pageData['priority'] = $this->getNewChapterPriority($newParent);
+        } else {
+            $pageData['priority'] = $this->getNewBookPriority($newParent);
+        }
+
+        return $this->publishPageDraft($copyPage, $pageData);
+    }
+
     /**
      * Saves a page revision into the system.
      * @param Page $page
     /**
      * Saves a page revision into the system.
      * @param Page $page
@@ -805,30 +866,6 @@ class EntityRepo
         return strip_tags($html);
     }
 
         return strip_tags($html);
     }
 
-    /**
-     * Get a new draft page instance.
-     * @param Book $book
-     * @param Chapter|bool $chapter
-     * @return Page
-     */
-    public function getDraftPage(Book $book, $chapter = false)
-    {
-        $page = $this->page->newInstance();
-        $page->name = trans('entities.pages_initial_name');
-        $page->created_by = user()->id;
-        $page->updated_by = user()->id;
-        $page->draft = true;
-
-        if ($chapter) {
-            $page->chapter_id = $chapter->id;
-        }
-
-        $book->pages()->save($page);
-        $page = $this->page->find($page->id);
-        $this->permissionService->buildJointPermissionsForEntity($page);
-        return $page;
-    }
-
     /**
      * Search for image usage within page content.
      * @param $imageString
     /**
      * Search for image usage within page content.
      * @param $imageString
index 331ed06c87e10e8e96fb098f63a0fd714eb4ce9e..e74801dc8b9aacc329ae64cb70605b719d2a1ebe 100644 (file)
@@ -630,16 +630,17 @@ class PermissionService
      * @param string $tableName
      * @param string $entityIdColumn
      * @param string $entityTypeColumn
      * @param string $tableName
      * @param string $entityIdColumn
      * @param string $entityTypeColumn
+     * @param string $action
      * @return mixed
      */
      * @return mixed
      */
-    public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
+    public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
     {
         if ($this->isAdmin()) {
             $this->clean();
             return $query;
         }
 
     {
         if ($this->isAdmin()) {
             $this->clean();
             return $query;
         }
 
-        $this->currentAction = 'view';
+        $this->currentAction = $action;
         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
 
         $q = $query->where(function ($query) use ($tableDetails) {
         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
 
         $q = $query->where(function ($query) use ($tableDetails) {
index 056e1f0772d3bc79f3bc3f788585875805e20e07..6390b8bc4582f3c348cf6a534544c25499ffb5d0 100644 (file)
@@ -67,7 +67,7 @@ class SearchService
      * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
      * @return array[int, Collection];
      */
      * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
      * @return array[int, Collection];
      */
-    public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
+    public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
     {
         $terms = $this->parseSearchString($searchString);
         $entityTypes = array_keys($this->entities);
     {
         $terms = $this->parseSearchString($searchString);
         $entityTypes = array_keys($this->entities);
@@ -87,8 +87,8 @@ class SearchService
             if (!in_array($entityType, $entityTypes)) {
                 continue;
             }
             if (!in_array($entityType, $entityTypes)) {
                 continue;
             }
-            $search = $this->searchEntityTable($terms, $entityType, $page, $count);
-            $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, true);
+            $search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
+            $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
             if ($entityTotal > $page * $count) {
                 $hasMore = true;
             }
             if ($entityTotal > $page * $count) {
                 $hasMore = true;
             }
@@ -147,12 +147,13 @@ class SearchService
      * @param string $entityType
      * @param int $page
      * @param int $count
      * @param string $entityType
      * @param int $page
      * @param int $count
+     * @param string $action
      * @param bool $getCount Return the total count of the search
      * @return \Illuminate\Database\Eloquent\Collection|int|static[]
      */
      * @param bool $getCount Return the total count of the search
      * @return \Illuminate\Database\Eloquent\Collection|int|static[]
      */
-    public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
+    public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
     {
     {
-        $query = $this->buildEntitySearchQuery($terms, $entityType);
+        $query = $this->buildEntitySearchQuery($terms, $entityType, $action);
         if ($getCount) {
             return $query->count();
         }
         if ($getCount) {
             return $query->count();
         }
@@ -165,9 +166,10 @@ class SearchService
      * Create a search query for an entity
      * @param array $terms
      * @param string $entityType
      * Create a search query for an entity
      * @param array $terms
      * @param string $entityType
+     * @param string $action
      * @return \Illuminate\Database\Eloquent\Builder
      */
      * @return \Illuminate\Database\Eloquent\Builder
      */
-    protected function buildEntitySearchQuery($terms, $entityType = 'page')
+    protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
     {
         $entity = $this->getEntity($entityType);
         $entitySelect = $entity->newQuery();
     {
         $entity = $this->getEntity($entityType);
         $entitySelect = $entity->newQuery();
@@ -212,7 +214,7 @@ class SearchService
             }
         }
 
             }
         }
 
-        return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
+        return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
     }
 
 
     }
 
 
index ddcf2eb7eff42e13d2c9bff56444dbe645679213..cd869018c2839fd7213b731bed259c58b3aabb61 100644 (file)
@@ -51,11 +51,13 @@ class ViewService
      * @param int $count
      * @param int $page
      * @param bool|false|array $filterModel
      * @param int $count
      * @param int $page
      * @param bool|false|array $filterModel
+     * @param string $action - used for permission checking
+     * @return
      */
      */
-    public function getPopular($count = 10, $page = 0, $filterModel = false)
+    public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
     {
         $skipCount = $count * $page;
     {
         $skipCount = $count * $page;
-        $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
+        $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
             ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
             ->groupBy('viewable_id', 'viewable_type')
             ->orderBy('view_count', 'desc');
             ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
             ->groupBy('viewable_id', 'viewable_type')
             ->orderBy('view_count', 'desc');
index c794a62dae93319f0dfde2bad1b1ee45ae531b99..30f58c000f4a7c42136e4c4716dd7dba1132a581 100644 (file)
         }
       }
     },
         }
       }
     },
-    "moment": {
-      "version": "2.21.0",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz",
-      "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ=="
-    },
     "move-concurrently": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
     "move-concurrently": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
index 53358378a31aa5dc6303e995f48a429be8d88724..5bd0d54978db33e51f81a59f5cdbc44be195a8be 100644 (file)
@@ -7,7 +7,8 @@ class EntitySelector {
         this.lastClick = 0;
 
         let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
         this.lastClick = 0;
 
         let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
-        this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`);
+        let entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
+        this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
 
         this.input = elem.querySelector('[entity-selector-input]');
         this.searchInput = elem.querySelector('[entity-selector-search]');
 
         this.input = elem.querySelector('[entity-selector-input]');
         this.searchInput = elem.querySelector('[entity-selector-search]');
@@ -68,7 +69,6 @@ class EntitySelector {
 
     onClick(event) {
         let t = event.target;
 
     onClick(event) {
         let t = event.target;
-        console.log('click', t);
 
         if (t.matches('.entity-list-item  *')) {
             event.preventDefault();
 
         if (t.matches('.entity-list-item  *')) {
             event.preventDefault();
index 26f096327a324b21d2bd9fb3eeddeaf6990cafc2..c2744d906285b60af537af44f0fd2dfd566baa8e 100644 (file)
@@ -31,6 +31,7 @@ return [
     'edit' => 'Edit',
     'sort' => 'Sort',
     'move' => 'Move',
     'edit' => 'Edit',
     'sort' => 'Sort',
     'move' => 'Move',
+    'copy' => 'Copy',
     'reply' => 'Reply',
     'delete' => 'Delete',
     'search' => 'Search',
     'reply' => 'Reply',
     'delete' => 'Delete',
     'search' => 'Search',
index a4d3ae6e8ccb7b2c6eb002d914431ec2e1014bae..430655a87be82cd4993b6b822b0751923a5a7ad5 100644 (file)
@@ -166,6 +166,9 @@ return [
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
+    'pages_copy' => 'Copy Page',
+    'pages_copy_desination' => 'Copy Destination',
+    'pages_copy_success' => 'Page successfully copied',
     'pages_permissions' => 'Page Permissions',
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
     'pages_permissions' => 'Page Permissions',
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
index a47c26e261f00647d37abcad9c10bbb4897481ba..bf94b5b076ed189a3dc723ad6ce5013522daf049 100644 (file)
@@ -30,9 +30,9 @@
     </div>
 </div>
 
     </div>
 </div>
 
-<div class="form-group" collapsible id="logo-control">
+<div class="form-group" collapsible id="tags-control">
     <div class="collapse-title text-primary" collapsible-trigger>
     <div class="collapse-title text-primary" collapsible-trigger>
-        <label for="user-avatar">{{ trans('entities.book_tags') }}</label>
+        <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
     </div>
     <div class="collapse-content" collapsible-content>
         @include('components.tag-manager', ['entity' => isset($book)?$book:null, 'entityType' => 'chapter'])
     </div>
     <div class="collapse-content" collapsible-content>
         @include('components.tag-manager', ['entity' => isset($book)?$book:null, 'entityType' => 'chapter'])
index 03e2066ed194b9f6ad597a53d497f8b0bb88745e..89c574c28c0f3090868cc43ef1f3c6794f7b6d4c 100644 (file)
@@ -1,5 +1,5 @@
 <div class="form-group">
 <div class="form-group">
-    <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
+    <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}" entity-permission="{{ $entityPermission or 'view' }}">
         <input type="hidden" entity-selector-input name="{{$name}}" value="">
         <input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
         <div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
         <input type="hidden" entity-selector-input name="{{$name}}" value="">
         <input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
         <div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
diff --git a/resources/views/pages/copy.blade.php b/resources/views/pages/copy.blade.php
new file mode 100644 (file)
index 0000000..eb6afca
--- /dev/null
@@ -0,0 +1,43 @@
+@extends('simple-layout')
+
+@section('toolbar')
+    <div class="col-sm-12 faded">
+        @include('pages._breadcrumbs', ['page' => $page])
+    </div>
+@stop
+
+@section('body')
+
+    <div class="container small">
+        <p>&nbsp;</p>
+        <div class="card">
+            <h3>@icon('copy') {{ trans('entities.pages_copy') }}</h3>
+            <div class="body">
+                <form action="{{ $page->getUrl('/copy') }}" method="POST">
+                    {!! csrf_field() !!}
+
+                    <div class="form-group title-input">
+                        <label for="name">{{ trans('common.name') }}</label>
+                        @include('form/text', ['name' => 'name'])
+                    </div>
+
+                    <div class="form-group" collapsible>
+                        <div class="collapse-title text-primary" collapsible-trigger>
+                            <label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
+                        </div>
+                        <div class="collapse-content" collapsible-content>
+                            @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
+                        </div>
+                    </div>
+
+
+                    <div class="form-group text-right">
+                        <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
+                        <button type="submit" class="button pos">{{ trans('entities.pages_copy') }}</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+@stop
index dabc7b965ed9cfe1ecba66490069e68a711b8b5d..288de3d84acd759077142d4c329dc690b765329d 100644 (file)
@@ -22,6 +22,7 @@
                     <a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
                     <ul>
                         @if(userCan('page-update', $page))
                     <a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
                     <ul>
                         @if(userCan('page-update', $page))
+                            <li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
                             <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
                             <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
                         @endif
                             <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
                             <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
                         @endif
index be05cda90ad87a50cf14dc74e75568e92ae52b92..f7b2347a5d74a7c091653299270b599d2e816359 100644 (file)
@@ -47,6 +47,8 @@ Route::group(['middleware' => 'auth'], function () {
         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}/edit', 'PageController@edit');
         Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
         Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
+        Route::get('/{bookSlug}/page/{pageSlug}/copy', 'PageController@showCopy');
+        Route::post('/{bookSlug}/page/{pageSlug}/copy', 'PageController@copy');
         Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
         Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
         Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
         Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
         Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
         Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
index 3b0831029c0ecb7dafc97df41651911057b02e40..6e2b7c34e270a9047fa16f73eec3c6b1d1df24fd 100644 (file)
@@ -1,6 +1,7 @@
 <?php namespace Tests;
 
 use BookStack\Book;
 <?php namespace Tests;
 
 use BookStack\Book;
+use BookStack\Chapter;
 use BookStack\Page;
 use BookStack\Repos\EntityRepo;
 
 use BookStack\Page;
 use BookStack\Repos\EntityRepo;
 
@@ -11,7 +12,7 @@ class SortTest extends TestCase
     public function setUp()
     {
         parent::setUp();
     public function setUp()
     {
         parent::setUp();
-        $this->book = \BookStack\Book::first();
+        $this->book = Book::first();
     }
 
     public function test_drafts_do_not_show_up()
     }
 
     public function test_drafts_do_not_show_up()
@@ -29,9 +30,9 @@ class SortTest extends TestCase
 
     public function test_page_move()
     {
 
     public function test_page_move()
     {
-        $page = \BookStack\Page::first();
+        $page = Page::first();
         $currentBook = $page->book;
         $currentBook = $page->book;
-        $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
 
         $resp = $this->asAdmin()->get($page->getUrl() . '/move');
         $resp->assertSee('Move Page');
 
         $resp = $this->asAdmin()->get($page->getUrl() . '/move');
         $resp->assertSee('Move Page');
@@ -39,7 +40,7 @@ class SortTest extends TestCase
         $movePageResp = $this->put($page->getUrl() . '/move', [
             'entity_selection' => 'book:' . $newBook->id
         ]);
         $movePageResp = $this->put($page->getUrl() . '/move', [
             'entity_selection' => 'book:' . $newBook->id
         ]);
-        $page = \BookStack\Page::find($page->id);
+        $page = Page::find($page->id);
 
         $movePageResp->assertRedirect($page->getUrl());
         $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
 
         $movePageResp->assertRedirect($page->getUrl());
         $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
@@ -51,10 +52,10 @@ class SortTest extends TestCase
 
     public function test_chapter_move()
     {
 
     public function test_chapter_move()
     {
-        $chapter = \BookStack\Chapter::first();
+        $chapter = Chapter::first();
         $currentBook = $chapter->book;
         $pageToCheck = $chapter->pages->first();
         $currentBook = $chapter->book;
         $pageToCheck = $chapter->pages->first();
-        $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
 
         $chapterMoveResp = $this->asAdmin()->get($chapter->getUrl() . '/move');
         $chapterMoveResp->assertSee('Move Chapter');
 
         $chapterMoveResp = $this->asAdmin()->get($chapter->getUrl() . '/move');
         $chapterMoveResp->assertSee('Move Chapter');
@@ -63,7 +64,7 @@ class SortTest extends TestCase
             'entity_selection' => 'book:' . $newBook->id
         ]);
 
             'entity_selection' => 'book:' . $newBook->id
         ]);
 
-        $chapter = \BookStack\Chapter::find($chapter->id);
+        $chapter = Chapter::find($chapter->id);
         $moveChapterResp->assertRedirect($chapter->getUrl());
         $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');
 
         $moveChapterResp->assertRedirect($chapter->getUrl());
         $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');
 
@@ -71,7 +72,7 @@ class SortTest extends TestCase
         $newBookResp->assertSee('moved chapter');
         $newBookResp->assertSee($chapter->name);
 
         $newBookResp->assertSee('moved chapter');
         $newBookResp->assertSee($chapter->name);
 
-        $pageToCheck = \BookStack\Page::find($pageToCheck->id);
+        $pageToCheck = Page::find($pageToCheck->id);
         $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book');
         $pageCheckResp = $this->get($pageToCheck->getUrl());
         $pageCheckResp->assertSee($newBook->name);
         $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book');
         $pageCheckResp = $this->get($pageToCheck->getUrl());
         $pageCheckResp->assertSee($newBook->name);
@@ -120,4 +121,43 @@ class SortTest extends TestCase
         $checkResp->assertSee($newBook->name);
     }
 
         $checkResp->assertSee($newBook->name);
     }
 
+    public function test_page_copy()
+    {
+        $page = Page::first();
+        $currentBook = $page->book;
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
+
+        $resp = $this->asEditor()->get($page->getUrl('/copy'));
+        $resp->assertSee('Copy Page');
+
+        $movePageResp = $this->post($page->getUrl('/copy'), [
+            'entity_selection' => 'book:' . $newBook->id,
+            'name' => 'My copied test page'
+        ]);
+
+        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+        $movePageResp->assertRedirect($pageCopy->getUrl());
+        $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book');
+    }
+
+    public function test_page_copy_with_no_destination()
+    {
+        $page = Page::first();
+        $currentBook = $page->book;
+
+        $resp = $this->asEditor()->get($page->getUrl('/copy'));
+        $resp->assertSee('Copy Page');
+
+        $movePageResp = $this->post($page->getUrl('/copy'), [
+            'name' => 'My copied test page'
+        ]);
+
+        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+        $movePageResp->assertRedirect($pageCopy->getUrl());
+        $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book');
+        $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
+    }
+
 }
\ No newline at end of file
 }
\ No newline at end of file