In 'More' menu alongside move.
Allows you to move if you have permission to create within the new
target parent.
Closes #673
* @param $path
* @return string
*/
- public function getUrl($path)
+ public function getUrl($path = '/')
{
- return '/';
+ return $path;
}
}
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
+ * @throws NotFoundException
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
{
$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()) .'}';
- $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();
- $entities = $this->viewService->getPopular(20, 0, $entityNames);
+ $entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
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.
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
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
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
+ * @param string $action
* @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;
}
- $this->currentAction = 'view';
+ $this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
* @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);
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;
}
* @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[]
*/
- 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();
}
* Create a search query for an entity
* @param array $terms
* @param string $entityType
+ * @param string $action
* @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();
}
}
- return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
+ return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
}
* @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;
- $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');
}
}
},
- "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",
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]');
onClick(event) {
let t = event.target;
- console.log('click', t);
if (t.matches('.entity-list-item *')) {
event.preventDefault();
'edit' => 'Edit',
'sort' => 'Sort',
'move' => 'Move',
+ 'copy' => 'Copy',
'reply' => 'Reply',
'delete' => 'Delete',
'search' => 'Search',
'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',
</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>
- <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 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>
--- /dev/null
+@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> </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
<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
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');
<?php namespace Tests;
use BookStack\Book;
+use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
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_page_move()
{
- $page = \BookStack\Page::first();
+ $page = Page::first();
$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');
$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');
public function test_chapter_move()
{
- $chapter = \BookStack\Chapter::first();
+ $chapter = Chapter::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');
'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');
$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);
$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