Merge branch 'master' into release
This commit is contained in:
		
						commit
						7113807f12
					
				
							
								
								
									
										12
									
								
								.travis.yml
								
								
								
								
							
							
						
						
									
										12
									
								
								.travis.yml
								
								
								
								
							| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
dist: trusty
 | 
			
		||||
sudo: required
 | 
			
		||||
language: php
 | 
			
		||||
php:
 | 
			
		||||
  - 7.0
 | 
			
		||||
| 
						 | 
				
			
			@ -5,15 +7,21 @@ php:
 | 
			
		|||
cache:
 | 
			
		||||
  directories:
 | 
			
		||||
    - vendor
 | 
			
		||||
    - node_modules
 | 
			
		||||
    - $HOME/.composer/cache
 | 
			
		||||
 | 
			
		||||
addons:
 | 
			
		||||
  mariadb: '10.0'
 | 
			
		||||
  apt:
 | 
			
		||||
    packages:
 | 
			
		||||
    - mysql-server-5.6
 | 
			
		||||
    - mysql-client-core-5.6
 | 
			
		||||
    - mysql-client-5.6
 | 
			
		||||
 | 
			
		||||
before_install:
 | 
			
		||||
  - npm install -g npm@latest
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
  - mysql -e 'create database `bookstack-test`;'
 | 
			
		||||
  - mysql -u root -e 'create database `bookstack-test`;'
 | 
			
		||||
  - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
 | 
			
		||||
  - phpenv config-rm xdebug.ini
 | 
			
		||||
  - composer self-update
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2016 Dan Brown
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ class Activity extends Model
 | 
			
		|||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function isSimilarTo($activityB) {
 | 
			
		||||
        return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id];
 | 
			
		||||
        return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ class ChapterController extends Controller
 | 
			
		|||
 | 
			
		||||
        $input = $request->all();
 | 
			
		||||
        $input['priority'] = $this->bookRepo->getNewPriority($book);
 | 
			
		||||
        $chapter = $this->chapterRepo->createFromInput($request->all(), $book);
 | 
			
		||||
        $chapter = $this->chapterRepo->createFromInput($input, $book);
 | 
			
		||||
        Activity::add($chapter, 'chapter_create', $book->id);
 | 
			
		||||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -154,6 +154,63 @@ class ChapterController extends Controller
 | 
			
		|||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the page for moving a chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showMove($bookSlug, $chapterSlug) {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
        return view('chapters/move', [
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
            'book' => $book
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the move action for a chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function move($bookSlug, $chapterSlug, Request $request) {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
 | 
			
		||||
        $entitySelection = $request->get('entity_selection', null);
 | 
			
		||||
        if ($entitySelection === null || $entitySelection === '') {
 | 
			
		||||
            return redirect($chapter->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $stringExploded = explode(':', $entitySelection);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
        $entityId = intval($stringExploded[1]);
 | 
			
		||||
 | 
			
		||||
        $parent = false;
 | 
			
		||||
 | 
			
		||||
        if ($entityType == 'book') {
 | 
			
		||||
            $parent = $this->bookRepo->getById($entityId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($parent === false || $parent === null) {
 | 
			
		||||
            session()->flash('The selected Book was not found');
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->chapterRepo->changeBook($parent->id, $chapter);
 | 
			
		||||
        Activity::add($chapter, 'chapter_move', $chapter->book->id);
 | 
			
		||||
        session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
 | 
			
		||||
 | 
			
		||||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the Restrictions view.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,9 +51,9 @@ class ImageController extends Controller
 | 
			
		|||
        $this->validate($request, [
 | 
			
		||||
            'term' => 'required|string'
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        $searchTerm = $request->get('term');
 | 
			
		||||
        $imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
 | 
			
		||||
        $imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
 | 
			
		||||
        return response()->json($imgData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +99,7 @@ class ImageController extends Controller
 | 
			
		|||
    {
 | 
			
		||||
        $this->checkPermission('image-create-all');
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'file' => 'image|mimes:jpeg,gif,png'
 | 
			
		||||
            'file' => 'is_image'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $imageUpload = $request->file('file');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,7 +92,7 @@ class PageController extends Controller
 | 
			
		|||
 | 
			
		||||
        $draftPage = $this->pageRepo->getById($pageId, true);
 | 
			
		||||
 | 
			
		||||
        $chapterId = $draftPage->chapter_id;
 | 
			
		||||
        $chapterId = intval($draftPage->chapter_id);
 | 
			
		||||
        $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $parent);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -450,6 +450,67 @@ class PageController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the view to choose a new parent to move a page into.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $pageSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showMove($bookSlug, $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        return view('pages/move', [
 | 
			
		||||
            'book' => $book,
 | 
			
		||||
            'page' => $page
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Does the action of moving the location of a page
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $pageSlug
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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('search/entity-ajax-list', ['entities' => $entities]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,7 @@ class TagController extends Controller
 | 
			
		|||
     */
 | 
			
		||||
    public function getNameSuggestions(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $searchTerm = $request->get('search');
 | 
			
		||||
        $searchTerm = $request->has('search') ? $request->get('search') : false;
 | 
			
		||||
        $suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
 | 
			
		||||
        return response()->json($suggestions);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -66,8 +66,9 @@ class TagController extends Controller
 | 
			
		|||
     */
 | 
			
		||||
    public function getValueSuggestions(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $searchTerm = $request->get('search');
 | 
			
		||||
        $suggestions = $this->tagRepo->getValueSuggestions($searchTerm);
 | 
			
		||||
        $searchTerm = $request->has('search') ? $request->get('search') : false;
 | 
			
		||||
        $tagName = $request->has('name') ? $request->get('name') : false;
 | 
			
		||||
        $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
 | 
			
		||||
        return response()->json($suggestions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () {
 | 
			
		|||
        Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
 | 
			
		||||
        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');
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +55,8 @@ Route::group(['middleware' => 'auth'], function () {
 | 
			
		|||
        Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
 | 
			
		||||
        Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
 | 
			
		||||
        Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
 | 
			
		||||
        Route::get('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@showMove');
 | 
			
		||||
        Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
 | 
			
		||||
        Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
 | 
			
		||||
        Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict');
 | 
			
		||||
        Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +97,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');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,12 @@ class AppServiceProvider extends ServiceProvider
 | 
			
		|||
     */
 | 
			
		||||
    public function boot()
 | 
			
		||||
    {
 | 
			
		||||
        //
 | 
			
		||||
        // Custom validation methods
 | 
			
		||||
        \Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
 | 
			
		||||
            $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
 | 
			
		||||
            return in_array($value->getMimeType(), $imageMimes);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -251,7 +251,10 @@ class BookRepo extends EntityRepo
 | 
			
		|||
        }]);
 | 
			
		||||
        $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
 | 
			
		||||
        $chapters = $chapterQuery->get();
 | 
			
		||||
        $children = $pages->merge($chapters);
 | 
			
		||||
        $children = $pages->values();
 | 
			
		||||
        foreach ($chapters as $chapter) {
 | 
			
		||||
            $children->push($chapter);
 | 
			
		||||
        }
 | 
			
		||||
        $bookSlug = $book->slug;
 | 
			
		||||
 | 
			
		||||
        $children->each(function ($child) use ($bookSlug) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,18 @@ use BookStack\Chapter;
 | 
			
		|||
 | 
			
		||||
class ChapterRepo extends EntityRepo
 | 
			
		||||
{
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ChapterRepo constructor.
 | 
			
		||||
     * @param $pageRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(PageRepo $pageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->pageRepo = $pageRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Base query for getting chapters, Takes permissions into account.
 | 
			
		||||
     * @return mixed
 | 
			
		||||
| 
						 | 
				
			
			@ -189,12 +201,21 @@ class ChapterRepo extends EntityRepo
 | 
			
		|||
    public function changeBook($bookId, Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter->book_id = $bookId;
 | 
			
		||||
        // Update related activity
 | 
			
		||||
        foreach ($chapter->activity as $activity) {
 | 
			
		||||
            $activity->book_id = $bookId;
 | 
			
		||||
            $activity->save();
 | 
			
		||||
        }
 | 
			
		||||
        $chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
 | 
			
		||||
        $chapter->save();
 | 
			
		||||
        // Update all child pages
 | 
			
		||||
        foreach ($chapter->pages as $page) {
 | 
			
		||||
            $this->pageRepo->changeBook($bookId, $page);
 | 
			
		||||
        }
 | 
			
		||||
        // Update permissions
 | 
			
		||||
        $chapter->load('book');
 | 
			
		||||
        $this->permissionService->buildJointPermissionsForEntity($chapter->book);
 | 
			
		||||
 | 
			
		||||
        return $chapter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,29 +58,48 @@ class TagRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get tag name suggestions from scanning existing tag names.
 | 
			
		||||
     * If no search term is given the 50 most popular tag names are provided.
 | 
			
		||||
     * @param $searchTerm
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getNameSuggestions($searchTerm)
 | 
			
		||||
    public function getNameSuggestions($searchTerm = false)
 | 
			
		||||
    {
 | 
			
		||||
        if ($searchTerm === '') return [];
 | 
			
		||||
        $query = $this->tag->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc');
 | 
			
		||||
        $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
 | 
			
		||||
 | 
			
		||||
        if ($searchTerm) {
 | 
			
		||||
            $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
 | 
			
		||||
        } else {
 | 
			
		||||
            $query = $query->orderBy('count', 'desc')->take(50);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
 | 
			
		||||
        return $query->get(['name'])->pluck('name');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get tag value suggestions from scanning existing tag values.
 | 
			
		||||
     * If no search is given the 50 most popular values are provided.
 | 
			
		||||
     * Passing a tagName will only find values for a tags with a particular name.
 | 
			
		||||
     * @param $searchTerm
 | 
			
		||||
     * @param $tagName
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getValueSuggestions($searchTerm)
 | 
			
		||||
    public function getValueSuggestions($searchTerm = false, $tagName = false)
 | 
			
		||||
    {
 | 
			
		||||
        if ($searchTerm === '') return [];
 | 
			
		||||
        $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc');
 | 
			
		||||
        $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
 | 
			
		||||
 | 
			
		||||
        if ($searchTerm) {
 | 
			
		||||
            $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
 | 
			
		||||
        } else {
 | 
			
		||||
            $query = $query->orderBy('count', 'desc')->take(50);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($tagName !== false) $query = $query->where('name', '=', $tagName);
 | 
			
		||||
 | 
			
		||||
        $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
 | 
			
		||||
        return $query->get(['value'])->pluck('value');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save an array of tags to an entity
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ class ActivityService
 | 
			
		|||
    {
 | 
			
		||||
        $activityList = $this->permissionService
 | 
			
		||||
            ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
 | 
			
		||||
            ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
 | 
			
		||||
            ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
 | 
			
		||||
 | 
			
		||||
        return $this->filterSimilar($activityList);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ use BookStack\Book;
 | 
			
		|||
use BookStack\Chapter;
 | 
			
		||||
use BookStack\Entity;
 | 
			
		||||
use BookStack\JointPermission;
 | 
			
		||||
use BookStack\Ownable;
 | 
			
		||||
use BookStack\Page;
 | 
			
		||||
use BookStack\Role;
 | 
			
		||||
use BookStack\User;
 | 
			
		||||
| 
						 | 
				
			
			@ -307,16 +308,16 @@ class PermissionService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if an entity has a restriction set upon it.
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
     * @param Ownable $ownable
 | 
			
		||||
     * @param $permission
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function checkEntityUserAccess(Entity $entity, $permission)
 | 
			
		||||
    public function checkOwnableUserAccess(Ownable $ownable, $permission)
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->isAdmin) return true;
 | 
			
		||||
        $explodedPermission = explode('-', $permission);
 | 
			
		||||
 | 
			
		||||
        $baseQuery = $entity->where('id', '=', $entity->id);
 | 
			
		||||
        $baseQuery = $ownable->where('id', '=', $ownable->id);
 | 
			
		||||
        $action = end($explodedPermission);
 | 
			
		||||
        $this->currentAction = $action;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -327,7 +328,7 @@ class PermissionService
 | 
			
		|||
            $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
 | 
			
		||||
            $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
 | 
			
		||||
            $this->currentAction = 'view';
 | 
			
		||||
            $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by;
 | 
			
		||||
            $isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
 | 
			
		||||
            return ($allPermission || ($isOwner && $ownPermission));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use BookStack\Ownable;
 | 
			
		||||
 | 
			
		||||
if (!function_exists('versioned_asset')) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the path to a versioned file.
 | 
			
		||||
| 
						 | 
				
			
			@ -34,18 +36,18 @@ if (!function_exists('versioned_asset')) {
 | 
			
		|||
 * If an ownable element is passed in the jointPermissions are checked against
 | 
			
		||||
 * that particular item.
 | 
			
		||||
 * @param $permission
 | 
			
		||||
 * @param \BookStack\Ownable $ownable
 | 
			
		||||
 * @param Ownable $ownable
 | 
			
		||||
 * @return mixed
 | 
			
		||||
 */
 | 
			
		||||
function userCan($permission, \BookStack\Ownable $ownable = null)
 | 
			
		||||
function userCan($permission, Ownable $ownable = null)
 | 
			
		||||
{
 | 
			
		||||
    if ($ownable === null) {
 | 
			
		||||
        return auth()->user() && auth()->user()->can($permission);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check permission on ownable item
 | 
			
		||||
    $permissionService = app('BookStack\Services\PermissionService');
 | 
			
		||||
    return $permissionService->checkEntityUserAccess($ownable, $permission);
 | 
			
		||||
    $permissionService = app(\BookStack\Services\PermissionService::class);
 | 
			
		||||
    return $permissionService->checkOwnableUserAccess($ownable, $permission);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,8 @@
 | 
			
		|||
 */
 | 
			
		||||
return [
 | 
			
		||||
 | 
			
		||||
    'app-editor' => 'wysiwyg'
 | 
			
		||||
    'app-editor' => 'wysiwyg',
 | 
			
		||||
    'app-color'  => '#0288D1',
 | 
			
		||||
    'app-color-light' => 'rgba(21, 101, 192, 0.15)'
 | 
			
		||||
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,13 @@ class CreateBooksTable extends Migration
 | 
			
		|||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::create('books', function (Blueprint $table) {
 | 
			
		||||
        $pdo = \DB::connection()->getPdo();
 | 
			
		||||
        $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
 | 
			
		||||
        $requiresISAM = strpos($mysqlVersion, '5.5') === 0;
 | 
			
		||||
 | 
			
		||||
        Schema::create('books', function (Blueprint $table) use ($requiresISAM) {
 | 
			
		||||
	        if($requiresISAM) $table->engine = 'MyISAM';
 | 
			
		||||
            
 | 
			
		||||
            $table->increments('id');
 | 
			
		||||
            $table->string('name');
 | 
			
		||||
            $table->string('slug')->indexed();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,13 @@ class CreatePagesTable extends Migration
 | 
			
		|||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::create('pages', function (Blueprint $table) {
 | 
			
		||||
        $pdo = \DB::connection()->getPdo();
 | 
			
		||||
        $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
 | 
			
		||||
        $requiresISAM = strpos($mysqlVersion, '5.5') === 0;
 | 
			
		||||
 | 
			
		||||
        Schema::create('pages', function (Blueprint $table) use ($requiresISAM) {
 | 
			
		||||
            if($requiresISAM) $table->engine = 'MyISAM';
 | 
			
		||||
            
 | 
			
		||||
            $table->increments('id');
 | 
			
		||||
            $table->integer('book_id');
 | 
			
		||||
            $table->integer('chapter_id');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,12 @@ class CreateChaptersTable extends Migration
 | 
			
		|||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::create('chapters', function (Blueprint $table) {
 | 
			
		||||
        $pdo = \DB::connection()->getPdo();
 | 
			
		||||
        $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
 | 
			
		||||
        $requiresISAM = strpos($mysqlVersion, '5.5') === 0;
 | 
			
		||||
 | 
			
		||||
        Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) {
 | 
			
		||||
            if($requiresISAM) $table->engine = 'MyISAM';
 | 
			
		||||
            $table->increments('id');
 | 
			
		||||
            $table->integer('book_id');
 | 
			
		||||
            $table->string('slug')->indexed();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
# BookStack
 | 
			
		||||
 | 
			
		||||
[](https://github.com/ssddanbrown/BookStack/releases/latest)
 | 
			
		||||
[](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
 | 
			
		||||
[](https://travis-ci.org/ssddanbrown/BookStack)
 | 
			
		||||
 | 
			
		||||
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -379,6 +379,15 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
            saveDraft();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Listen to shortcuts coming via events
 | 
			
		||||
        $scope.$on('editor-keydown', (event, data) => {
 | 
			
		||||
            // Save shortcut (ctrl+s)
 | 
			
		||||
            if (data.keyCode == 83 && (navigator.platform.match("Mac") ? data.metaKey : data.ctrlKey)) {
 | 
			
		||||
                data.preventDefault();
 | 
			
		||||
                saveDraft();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Discard the current draft and grab the current page
 | 
			
		||||
         * content from the system via an AJAX request.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -149,7 +149,10 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
        };
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Dropdown
 | 
			
		||||
     * Provides some simple logic to create small dropdown menus
 | 
			
		||||
     */
 | 
			
		||||
    ngApp.directive('dropdown', [function () {
 | 
			
		||||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +169,11 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
        };
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
    ngApp.directive('tinymce', ['$timeout', function($timeout) {
 | 
			
		||||
    /**
 | 
			
		||||
     * TinyMCE
 | 
			
		||||
     * An angular wrapper around the tinyMCE editor.
 | 
			
		||||
     */
 | 
			
		||||
    ngApp.directive('tinymce', ['$timeout', function ($timeout) {
 | 
			
		||||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
            scope: {
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +192,10 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                        scope.mceChange(content);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    editor.on('keydown', (event) => {
 | 
			
		||||
                        scope.$emit('editor-keydown', event);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    editor.on('init', (e) => {
 | 
			
		||||
                        scope.mceModel = editor.getContent();
 | 
			
		||||
                    });
 | 
			
		||||
| 
						 | 
				
			
			@ -200,8 +211,8 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                scope.tinymce.extraSetups.push(tinyMceSetup);
 | 
			
		||||
 | 
			
		||||
                // Custom tinyMCE plugins
 | 
			
		||||
                tinymce.PluginManager.add('customhr', function(editor) {
 | 
			
		||||
                    editor.addCommand('InsertHorizontalRule', function() {
 | 
			
		||||
                tinymce.PluginManager.add('customhr', function (editor) {
 | 
			
		||||
                    editor.addCommand('InsertHorizontalRule', function () {
 | 
			
		||||
                        var hrElem = document.createElement('hr');
 | 
			
		||||
                        var cNode = editor.selection.getNode();
 | 
			
		||||
                        var parentNode = cNode.parentNode;
 | 
			
		||||
| 
						 | 
				
			
			@ -227,7 +238,11 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
        }
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
    ngApp.directive('markdownInput', ['$timeout', function($timeout) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Markdown input
 | 
			
		||||
     * Handles the logic for just the editor input field.
 | 
			
		||||
     */
 | 
			
		||||
    ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
 | 
			
		||||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
            scope: {
 | 
			
		||||
| 
						 | 
				
			
			@ -251,7 +266,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
 | 
			
		||||
                scope.$on('markdown-update', (event, value) => {
 | 
			
		||||
                    element.val(value);
 | 
			
		||||
                    scope.mdModel= value;
 | 
			
		||||
                    scope.mdModel = value;
 | 
			
		||||
                    scope.mdChange(markdown(value));
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -259,23 +274,59 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
        }
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
    ngApp.directive('markdownEditor', ['$timeout', function($timeout) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Markdown Editor
 | 
			
		||||
     * Handles all functionality of the markdown editor.
 | 
			
		||||
     */
 | 
			
		||||
    ngApp.directive('markdownEditor', ['$timeout', function ($timeout) {
 | 
			
		||||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
            link: function (scope, element, attrs) {
 | 
			
		||||
 | 
			
		||||
                // Elements
 | 
			
		||||
                var input = element.find('textarea[markdown-input]');
 | 
			
		||||
                var insertImage = element.find('button[data-action="insertImage"]');
 | 
			
		||||
                const input = element.find('textarea[markdown-input]');
 | 
			
		||||
                const display = element.find('.markdown-display').first();
 | 
			
		||||
                const insertImage = element.find('button[data-action="insertImage"]');
 | 
			
		||||
 | 
			
		||||
                var currentCaretPos = 0;
 | 
			
		||||
                let currentCaretPos = 0;
 | 
			
		||||
 | 
			
		||||
                input.blur((event) => {
 | 
			
		||||
                input.blur(event => {
 | 
			
		||||
                    currentCaretPos = input[0].selectionStart;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Insert image shortcut
 | 
			
		||||
                input.keydown((event) => {
 | 
			
		||||
                // Scroll sync
 | 
			
		||||
                let inputScrollHeight,
 | 
			
		||||
                    inputHeight,
 | 
			
		||||
                    displayScrollHeight,
 | 
			
		||||
                    displayHeight;
 | 
			
		||||
 | 
			
		||||
                function setScrollHeights() {
 | 
			
		||||
                    inputScrollHeight = input[0].scrollHeight;
 | 
			
		||||
                    inputHeight = input.height();
 | 
			
		||||
                    displayScrollHeight = display[0].scrollHeight;
 | 
			
		||||
                    displayHeight = display.height();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    setScrollHeights();
 | 
			
		||||
                }, 200);
 | 
			
		||||
                window.addEventListener('resize', setScrollHeights);
 | 
			
		||||
                let scrollDebounceTime = 800;
 | 
			
		||||
                let lastScroll = 0;
 | 
			
		||||
                input.on('scroll', event => {
 | 
			
		||||
                    let now = Date.now();
 | 
			
		||||
                    if (now - lastScroll > scrollDebounceTime) {
 | 
			
		||||
                        setScrollHeights()
 | 
			
		||||
                    }
 | 
			
		||||
                    let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight));
 | 
			
		||||
                    let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
 | 
			
		||||
                    display.scrollTop(displayScrollY);
 | 
			
		||||
                    lastScroll = now;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Editor key-presses
 | 
			
		||||
                input.keydown(event => {
 | 
			
		||||
                    // Insert image shortcut
 | 
			
		||||
                    if (event.which === 73 && event.ctrlKey && event.shiftKey) {
 | 
			
		||||
                        event.preventDefault();
 | 
			
		||||
                        var caretPos = input[0].selectionStart;
 | 
			
		||||
| 
						 | 
				
			
			@ -285,12 +336,15 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                        input.focus();
 | 
			
		||||
                        input[0].selectionStart = caretPos + (";
 | 
			
		||||
                        input[0].selectionEnd = caretPos + (';
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    // Pass key presses to controller via event
 | 
			
		||||
                    scope.$emit('editor-keydown', event);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Insert image from image manager
 | 
			
		||||
                insertImage.click((event) => {
 | 
			
		||||
                    window.ImageManager.showExternal((image) => {
 | 
			
		||||
                insertImage.click(event => {
 | 
			
		||||
                    window.ImageManager.showExternal(image => {
 | 
			
		||||
                        var caretPos = currentCaretPos;
 | 
			
		||||
                        var currentContent = input.val();
 | 
			
		||||
                        var mdImageText = "";
 | 
			
		||||
| 
						 | 
				
			
			@ -302,11 +356,16 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }]);
 | 
			
		||||
    
 | 
			
		||||
    ngApp.directive('toolbox', [function() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page Editor Toolbox
 | 
			
		||||
     * Controls all functionality for the sliding toolbox
 | 
			
		||||
     * on the page edit view.
 | 
			
		||||
     */
 | 
			
		||||
    ngApp.directive('toolbox', [function () {
 | 
			
		||||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
            link: function(scope, elem, attrs) {
 | 
			
		||||
            link: function (scope, elem, attrs) {
 | 
			
		||||
 | 
			
		||||
                // Get common elements
 | 
			
		||||
                const $buttons = elem.find('[tab-button]');
 | 
			
		||||
| 
						 | 
				
			
			@ -317,7 +376,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                $toggle.click((e) => {
 | 
			
		||||
                    elem.toggleClass('open');
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                // Set an active tab/content by name
 | 
			
		||||
                function setActive(tabName, openToolbox) {
 | 
			
		||||
                    $buttons.removeClass('active');
 | 
			
		||||
| 
						 | 
				
			
			@ -331,7 +390,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                setActive($content.first().attr('tab-content'), false);
 | 
			
		||||
 | 
			
		||||
                // Handle tab button click
 | 
			
		||||
                $buttons.click(function(e) {
 | 
			
		||||
                $buttons.click(function (e) {
 | 
			
		||||
                    let name = $(this).attr('tab-button');
 | 
			
		||||
                    setActive(name, true);
 | 
			
		||||
                });
 | 
			
		||||
| 
						 | 
				
			
			@ -339,11 +398,16 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
        }
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
    ngApp.directive('autosuggestions', ['$http', function($http) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Tag Autosuggestions
 | 
			
		||||
     * Listens to child inputs and provides autosuggestions depending on field type
 | 
			
		||||
     * and input. Suggestions provided by server.
 | 
			
		||||
     */
 | 
			
		||||
    ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
 | 
			
		||||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
            link: function(scope, elem, attrs) {
 | 
			
		||||
                
 | 
			
		||||
            link: function (scope, elem, attrs) {
 | 
			
		||||
 | 
			
		||||
                // Local storage for quick caching.
 | 
			
		||||
                const localCache = {};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -360,38 +424,49 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                let active = 0;
 | 
			
		||||
 | 
			
		||||
                // Listen to input events on autosuggest fields
 | 
			
		||||
                elem.on('input', '[autosuggest]', function(event) {
 | 
			
		||||
                elem.on('input focus', '[autosuggest]', function (event) {
 | 
			
		||||
                    let $input = $(this);
 | 
			
		||||
                    let val = $input.val();
 | 
			
		||||
                    let url = $input.attr('autosuggest');
 | 
			
		||||
                    // No suggestions until at least 3 chars
 | 
			
		||||
                    if (val.length < 3) {
 | 
			
		||||
                        if (isShowing) {
 | 
			
		||||
                            $suggestionBox.hide();
 | 
			
		||||
                            isShowing = false;
 | 
			
		||||
                    let type = $input.attr('autosuggest-type');
 | 
			
		||||
                    
 | 
			
		||||
                    // Add name param to request if for a value
 | 
			
		||||
                    if (type.toLowerCase() === 'value') {
 | 
			
		||||
                        let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
 | 
			
		||||
                        let nameVal = $nameInput.val();
 | 
			
		||||
                        if (nameVal !== '') {
 | 
			
		||||
                            url += '?name=' + encodeURIComponent(nameVal);
 | 
			
		||||
                        }
 | 
			
		||||
                        return;
 | 
			
		||||
                    };
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let suggestionPromise = getSuggestions(val.slice(0, 3), url);
 | 
			
		||||
                    suggestionPromise.then((suggestions) => {
 | 
			
		||||
                       if (val.length > 2) {
 | 
			
		||||
                           suggestions = suggestions.filter((item) => {
 | 
			
		||||
                               return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
 | 
			
		||||
                           }).slice(0, 4);
 | 
			
		||||
                           displaySuggestions($input, suggestions);
 | 
			
		||||
                       }
 | 
			
		||||
                    suggestionPromise.then(suggestions => {
 | 
			
		||||
                        if (val.length === 0) {
 | 
			
		||||
                            displaySuggestions($input, suggestions.slice(0, 6));
 | 
			
		||||
                        } else  {
 | 
			
		||||
                            suggestions = suggestions.filter(item => {
 | 
			
		||||
                                return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
 | 
			
		||||
                            }).slice(0, 4);
 | 
			
		||||
                            displaySuggestions($input, suggestions);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Hide autosuggestions when input loses focus.
 | 
			
		||||
                // Slight delay to allow clicks.
 | 
			
		||||
                elem.on('blur', '[autosuggest]', function(event) {
 | 
			
		||||
                let lastFocusTime = 0;
 | 
			
		||||
                elem.on('blur', '[autosuggest]', function (event) {
 | 
			
		||||
                    let startTime = Date.now();
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        $suggestionBox.hide();
 | 
			
		||||
                        isShowing = false;
 | 
			
		||||
                        if (lastFocusTime < startTime) {
 | 
			
		||||
                            $suggestionBox.hide();
 | 
			
		||||
                            isShowing = false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }, 200)
 | 
			
		||||
                });
 | 
			
		||||
                elem.on('focus', '[autosuggest]', function (event) {
 | 
			
		||||
                    lastFocusTime = Date.now();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                elem.on('keydown', '[autosuggest]', function (event) {
 | 
			
		||||
                    if (!isShowing) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -401,23 +476,25 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
 | 
			
		||||
                    // Down arrow
 | 
			
		||||
                    if (event.keyCode === 40) {
 | 
			
		||||
                        let newActive = (active === suggestCount-1) ? 0 : active + 1;
 | 
			
		||||
                        let newActive = (active === suggestCount - 1) ? 0 : active + 1;
 | 
			
		||||
                        changeActiveTo(newActive, suggestionElems);
 | 
			
		||||
                    }
 | 
			
		||||
                    // Up arrow
 | 
			
		||||
                    else if (event.keyCode === 38) {
 | 
			
		||||
                        let newActive = (active === 0) ? suggestCount-1 : active - 1;
 | 
			
		||||
                        let newActive = (active === 0) ? suggestCount - 1 : active - 1;
 | 
			
		||||
                        changeActiveTo(newActive, suggestionElems);
 | 
			
		||||
                    }
 | 
			
		||||
                    // Enter key
 | 
			
		||||
                    else if (event.keyCode === 13) {
 | 
			
		||||
                    // Enter or tab key
 | 
			
		||||
                    else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
 | 
			
		||||
                        let text = suggestionElems[active].textContent;
 | 
			
		||||
                        currentInput[0].value = text;
 | 
			
		||||
                        currentInput.focus();
 | 
			
		||||
                        $suggestionBox.hide();
 | 
			
		||||
                        isShowing = false;
 | 
			
		||||
                        event.preventDefault();
 | 
			
		||||
                        return false;
 | 
			
		||||
                        if (event.keyCode === 13) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -430,6 +507,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
 | 
			
		||||
                // Display suggestions on a field
 | 
			
		||||
                let prevSuggestions = [];
 | 
			
		||||
 | 
			
		||||
                function displaySuggestions($input, suggestions) {
 | 
			
		||||
 | 
			
		||||
                    // Hide if no suggestions
 | 
			
		||||
| 
						 | 
				
			
			@ -466,7 +544,8 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                        if (i === 0) {
 | 
			
		||||
                            suggestion.className = 'active'
 | 
			
		||||
                            active = 0;
 | 
			
		||||
                        };
 | 
			
		||||
                        }
 | 
			
		||||
                        ;
 | 
			
		||||
                        $suggestionBox[0].appendChild(suggestion);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -484,17 +563,18 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
 | 
			
		||||
                // Get suggestions & cache
 | 
			
		||||
                function getSuggestions(input, url) {
 | 
			
		||||
                    let searchUrl = url + '?search=' + encodeURIComponent(input);
 | 
			
		||||
                    let hasQuery = url.indexOf('?') !== -1;
 | 
			
		||||
                    let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
 | 
			
		||||
 | 
			
		||||
                    // Get from local cache if exists
 | 
			
		||||
                    if (localCache[searchUrl]) {
 | 
			
		||||
                    if (typeof localCache[searchUrl] !== 'undefined') {
 | 
			
		||||
                        return new Promise((resolve, reject) => {
 | 
			
		||||
                            resolve(localCache[input]);
 | 
			
		||||
                            resolve(localCache[searchUrl]);
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $http.get(searchUrl).then((response) => {
 | 
			
		||||
                        localCache[input] = response.data;
 | 
			
		||||
                    return $http.get(searchUrl).then(response => {
 | 
			
		||||
                        localCache[searchUrl] = response.data;
 | 
			
		||||
                        return response.data;
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -502,6 +582,67 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
                    });
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -112,16 +112,11 @@ $(function () {
 | 
			
		|||
 | 
			
		||||
    // Common jQuery actions
 | 
			
		||||
    $('[data-action="expand-entity-list-details"]').click(function() {
 | 
			
		||||
        $('.entity-list.compact').find('p').slideToggle(240);
 | 
			
		||||
        $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function elemExists(selector) {
 | 
			
		||||
    return document.querySelector(selector) !== null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Page specific items
 | 
			
		||||
require('./pages/page-show');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
var mceOptions = module.exports = {
 | 
			
		||||
    selector: '#html-editor',
 | 
			
		||||
    content_css: [
 | 
			
		||||
        '/css/styles.css'
 | 
			
		||||
        '/css/styles.css',
 | 
			
		||||
        '/libs/material-design-iconic-font/css/material-design-iconic-font.min.css'
 | 
			
		||||
    ],
 | 
			
		||||
    body_class: 'page-content',
 | 
			
		||||
    relative_urls: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -19,11 +20,18 @@ var mceOptions = module.exports = {
 | 
			
		|||
        {title: "Header 1", format: "h1"},
 | 
			
		||||
        {title: "Header 2", format: "h2"},
 | 
			
		||||
        {title: "Header 3", format: "h3"},
 | 
			
		||||
        {title: "Paragraph", format: "p"},
 | 
			
		||||
        {title: "Paragraph", format: "p", exact: true, classes: ''},
 | 
			
		||||
        {title: "Blockquote", format: "blockquote"},
 | 
			
		||||
        {title: "Code Block", icon: "code", format: "pre"},
 | 
			
		||||
        {title: "Inline Code", icon: "code", inline: "code"}
 | 
			
		||||
        {title: "Inline Code", icon: "code", inline: "code"},
 | 
			
		||||
        {title: "Callouts", items: [
 | 
			
		||||
            {title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}},
 | 
			
		||||
            {title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}},
 | 
			
		||||
            {title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}},
 | 
			
		||||
            {title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}}
 | 
			
		||||
        ]}
 | 
			
		||||
    ],
 | 
			
		||||
    style_formats_merge: false,
 | 
			
		||||
    formats: {
 | 
			
		||||
        alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
 | 
			
		||||
        aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,15 +74,15 @@ window.setupPageShow = module.exports = function (pageId) {
 | 
			
		|||
    // Make the book-tree sidebar stick in view on scroll
 | 
			
		||||
    var $window = $(window);
 | 
			
		||||
    var $bookTree = $(".book-tree");
 | 
			
		||||
    var $bookTreeParent = $bookTree.parent();
 | 
			
		||||
    // Check the page is scrollable and the content is taller than the tree
 | 
			
		||||
    var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
 | 
			
		||||
    // Get current tree's width and header height
 | 
			
		||||
    var headerHeight = $("#header").height() + $(".toolbar").height();
 | 
			
		||||
    var isFixed = $window.scrollTop() > headerHeight;
 | 
			
		||||
    var bookTreeWidth = $bookTree.width();
 | 
			
		||||
    // Function to fix the tree as a sidebar
 | 
			
		||||
    function stickTree() {
 | 
			
		||||
        $bookTree.width(bookTreeWidth + 48 + 15);
 | 
			
		||||
        $bookTree.width($bookTreeParent.width() + 15);
 | 
			
		||||
        $bookTree.addClass("fixed");
 | 
			
		||||
        isFixed = true;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -101,13 +101,27 @@ window.setupPageShow = module.exports = function (pageId) {
 | 
			
		|||
            unstickTree();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    // The event ran when the window scrolls
 | 
			
		||||
    function windowScrollEvent() {
 | 
			
		||||
        checkTreeStickiness(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the page is scrollable and the window is wide enough listen to scroll events
 | 
			
		||||
    // and evaluate tree stickiness.
 | 
			
		||||
    if (pageScrollable && $window.width() > 1000) {
 | 
			
		||||
        $window.scroll(function() {
 | 
			
		||||
            checkTreeStickiness(false);
 | 
			
		||||
        });
 | 
			
		||||
        $window.on('scroll', windowScrollEvent);
 | 
			
		||||
        checkTreeStickiness(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle window resizing and switch between desktop/mobile views
 | 
			
		||||
    $window.on('resize', event => {
 | 
			
		||||
        if (pageScrollable && $window.width() > 1000) {
 | 
			
		||||
            $window.on('scroll', windowScrollEvent);
 | 
			
		||||
            checkTreeStickiness(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            $window.off('scroll', windowScrollEvent);
 | 
			
		||||
            unstickTree();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,3 +125,51 @@
 | 
			
		|||
    margin-right: $-xl;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Callouts
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.callout {
 | 
			
		||||
  border-left: 3px solid #BBB;
 | 
			
		||||
  background-color: #EEE;
 | 
			
		||||
  padding: $-s;
 | 
			
		||||
  &:before {
 | 
			
		||||
    font-family: 'Material-Design-Iconic-Font';
 | 
			
		||||
    padding-right: $-s;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
  }
 | 
			
		||||
  &.success {
 | 
			
		||||
    border-left-color: $positive;
 | 
			
		||||
    background-color: lighten($positive, 45%);
 | 
			
		||||
    color: darken($positive, 16%);
 | 
			
		||||
  }
 | 
			
		||||
  &.success:before {
 | 
			
		||||
    content: '\f269';
 | 
			
		||||
  }
 | 
			
		||||
  &.danger {
 | 
			
		||||
    border-left-color: $negative;
 | 
			
		||||
    background-color: lighten($negative, 34%);
 | 
			
		||||
    color: darken($negative, 20%);
 | 
			
		||||
  }
 | 
			
		||||
  &.danger:before {
 | 
			
		||||
    content: '\f1f2';
 | 
			
		||||
  }
 | 
			
		||||
  &.info {
 | 
			
		||||
    border-left-color: $info;
 | 
			
		||||
    background-color: lighten($info, 50%);
 | 
			
		||||
    color: darken($info, 16%);
 | 
			
		||||
  }
 | 
			
		||||
  &.info:before {
 | 
			
		||||
    content: '\f1f8';
 | 
			
		||||
  }
 | 
			
		||||
  &.warning {
 | 
			
		||||
    border-left-color: $warning;
 | 
			
		||||
    background-color: lighten($warning, 36%);
 | 
			
		||||
    color: darken($warning, 16%);
 | 
			
		||||
  }
 | 
			
		||||
  &.warning:before {
 | 
			
		||||
    content: '\f1f1';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +20,9 @@
 | 
			
		|||
  &.disabled, &[disabled] {
 | 
			
		||||
    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
 | 
			
		||||
  }
 | 
			
		||||
  &:focus {
 | 
			
		||||
    outline: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#html-editor {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
.page-list {
 | 
			
		||||
  h3 {
 | 
			
		||||
    margin: $-l 0 $-m 0;
 | 
			
		||||
    margin: $-l 0 $-xs 0;
 | 
			
		||||
    font-size: 1.666em;
 | 
			
		||||
  }
 | 
			
		||||
  a.chapter {
 | 
			
		||||
    color: $color-chapter;
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +9,6 @@
 | 
			
		|||
  .inset-list {
 | 
			
		||||
    display: none;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    // padding-left: $-m;
 | 
			
		||||
    margin-bottom: $-l;
 | 
			
		||||
  }
 | 
			
		||||
  h4 {
 | 
			
		||||
| 
						 | 
				
			
			@ -338,6 +338,10 @@ ul.pagination {
 | 
			
		|||
    padding-top: $-xs;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
  > p.empty-text {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-size: $fs-m;
 | 
			
		||||
  }
 | 
			
		||||
  hr {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,7 +48,7 @@
 | 
			
		|||
    max-width: 100%;
 | 
			
		||||
    height:auto;
 | 
			
		||||
  }
 | 
			
		||||
  h1, h2, h3, h4, h5, h6 {
 | 
			
		||||
  h1, h2, h3, h4, h5, h6, pre {
 | 
			
		||||
    clear: left;
 | 
			
		||||
  }
 | 
			
		||||
  hr {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
h1 {
 | 
			
		||||
  font-size: 3.625em;
 | 
			
		||||
  font-size: 3.425em;
 | 
			
		||||
  line-height: 1.22222222em;
 | 
			
		||||
  margin-top: 0.48888889em;
 | 
			
		||||
  margin-bottom: 0.48888889em;
 | 
			
		||||
| 
						 | 
				
			
			@ -33,10 +33,10 @@ h1, h2, h3, h4 {
 | 
			
		|||
  display: block;
 | 
			
		||||
  color: #555;
 | 
			
		||||
  .subheader {
 | 
			
		||||
    display: block;
 | 
			
		||||
    //display: block;
 | 
			
		||||
    font-size: 0.5em;
 | 
			
		||||
    line-height: 1em;
 | 
			
		||||
    color: lighten($text-dark, 16%);
 | 
			
		||||
    color: lighten($text-dark, 32%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -225,6 +225,15 @@ p.secondary, p .secondary, span.secondary, .text-secondary {
 | 
			
		|||
    color: $color-chapter;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.faded .text-book:hover {
 | 
			
		||||
  color: $color-book !important;
 | 
			
		||||
}
 | 
			
		||||
.faded .text-chapter:hover {
 | 
			
		||||
  color: $color-chapter !important;
 | 
			
		||||
}
 | 
			
		||||
.faded .text-page:hover {
 | 
			
		||||
  color: $color-page !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.highlight {
 | 
			
		||||
  //background-color: rgba($primary, 0.2);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ $primary-dark: #0288D1;
 | 
			
		|||
$secondary: #e27b41;
 | 
			
		||||
$positive: #52A256;
 | 
			
		||||
$negative: #E84F4F;
 | 
			
		||||
$info: $primary;
 | 
			
		||||
$warning: $secondary;
 | 
			
		||||
$primary-faded: rgba(21, 101, 192, 0.15);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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, span {
 | 
			
		||||
    color: #EEE;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ return [
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Activity text strings.
 | 
			
		||||
     * Is used for all the text within activity logs.
 | 
			
		||||
     * Is used for all the text within activity logs & notifications.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    // Pages
 | 
			
		||||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +25,7 @@ return [
 | 
			
		|||
    'chapter_update_notification' => 'Chapter Successfully Updated',
 | 
			
		||||
    'chapter_delete'              => 'deleted chapter',
 | 
			
		||||
    'chapter_delete_notification' => 'Chapter Successfully Deleted',
 | 
			
		||||
    'chapter_move'                => 'moved chapter',
 | 
			
		||||
 | 
			
		||||
    // Books
 | 
			
		||||
    'book_create'                 => 'created book',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
<div class="book">
 | 
			
		||||
<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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,11 @@
 | 
			
		|||
<div class="chapter">
 | 
			
		||||
<div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
 | 
			
		||||
    <h3>
 | 
			
		||||
        @if (isset($showPath) && $showPath)
 | 
			
		||||
            <a href="{{ $chapter->book->getUrl() }}" class="text-book">
 | 
			
		||||
                <i class="zmdi zmdi-book"></i>{{ $chapter->book->name }}
 | 
			
		||||
            </a>
 | 
			
		||||
            <span class="text-muted">  »  </span>
 | 
			
		||||
        @endif
 | 
			
		||||
        <a href="{{ $chapter->getUrl() }}" class="text-chapter">
 | 
			
		||||
            <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }}
 | 
			
		||||
        </a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
@extends('base')
 | 
			
		||||
 | 
			
		||||
@section('content')
 | 
			
		||||
 | 
			
		||||
    <div class="faded-small toolbar">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-sm-12 faded">
 | 
			
		||||
                    <div class="breadcrumbs">
 | 
			
		||||
                        <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
 | 
			
		||||
                        <span class="sep">»</span>
 | 
			
		||||
                        <a href="{{$chapter->getUrl()}}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->getShortName() }}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <h1>Move Chapter <small class="subheader">{{$chapter->name}}</small></h1>
 | 
			
		||||
 | 
			
		||||
        <form action="{{ $chapter->getUrl() }}/move" method="POST">
 | 
			
		||||
            {!! csrf_field() !!}
 | 
			
		||||
            <input type="hidden" name="_method" value="PUT">
 | 
			
		||||
 | 
			
		||||
            @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
 | 
			
		||||
 | 
			
		||||
            <a href="{{ $chapter->getUrl() }}" class="button muted">Cancel</a>
 | 
			
		||||
            <button type="submit" class="button pos">Move Chapter</button>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			@ -2,15 +2,15 @@
 | 
			
		|||
 | 
			
		||||
@section('content')
 | 
			
		||||
 | 
			
		||||
    <div class="faded-small toolbar" ng-non-bindable>
 | 
			
		||||
    <div class="faded-small toolbar">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-md-4 faded">
 | 
			
		||||
                <div class="col-sm-8 faded" ng-non-bindable>
 | 
			
		||||
                    <div class="breadcrumbs">
 | 
			
		||||
                        <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-8 faded">
 | 
			
		||||
                <div class="col-sm-4 faded">
 | 
			
		||||
                    <div class="action-buttons">
 | 
			
		||||
                        @if(userCan('page-create', $chapter))
 | 
			
		||||
                            <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a>
 | 
			
		||||
| 
						 | 
				
			
			@ -18,11 +18,21 @@
 | 
			
		|||
                        @if(userCan('chapter-update', $chapter))
 | 
			
		||||
                            <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
 | 
			
		||||
                        @endif
 | 
			
		||||
                        @if(userCan('restrictions-manage', $chapter))
 | 
			
		||||
                            <a href="{{$chapter->getUrl()}}/permissions" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Permissions</a>
 | 
			
		||||
                        @endif
 | 
			
		||||
                        @if(userCan('chapter-delete', $chapter))
 | 
			
		||||
                            <a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
 | 
			
		||||
                        @if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
 | 
			
		||||
                            <div dropdown class="dropdown-container">
 | 
			
		||||
                                <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
 | 
			
		||||
                                <ul>
 | 
			
		||||
                                    @if(userCan('chapter-update', $chapter))
 | 
			
		||||
                                        <li><a href="{{$chapter->getUrl() . '/move'}}" class="text-primary"><i class="zmdi zmdi-folder"></i>Move</a></li>
 | 
			
		||||
                                    @endif
 | 
			
		||||
                                    @if(userCan('restrictions-manage', $chapter))
 | 
			
		||||
                                        <li><a href="{{$chapter->getUrl()}}/permissions" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li>
 | 
			
		||||
                                    @endif
 | 
			
		||||
                                    @if(userCan('chapter-delete', $chapter))
 | 
			
		||||
                                        <li><a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
 | 
			
		||||
                                    @endif
 | 
			
		||||
                                </ul>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        @endif
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,18 +34,30 @@
 | 
			
		|||
                @else
 | 
			
		||||
                    <h3>Recent Books</h3>
 | 
			
		||||
                @endif
 | 
			
		||||
                @include('partials/entity-list', ['entities' => $recents, 'style' => 'compact'])
 | 
			
		||||
                @include('partials/entity-list', [
 | 
			
		||||
                'entities' => $recents,
 | 
			
		||||
                'style' => 'compact',
 | 
			
		||||
                'emptyText' => $signedIn ? 'You have not viewed any pages' : 'No books have been created'
 | 
			
		||||
                ])
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="col-sm-4">
 | 
			
		||||
                <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3>
 | 
			
		||||
                <div id="recently-created-pages">
 | 
			
		||||
                    @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
 | 
			
		||||
                    @include('partials/entity-list', [
 | 
			
		||||
                    'entities' => $recentlyCreatedPages,
 | 
			
		||||
                    'style' => 'compact',
 | 
			
		||||
                    'emptyText' => 'No pages have been recently created'
 | 
			
		||||
                    ])
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3>
 | 
			
		||||
                <div id="recently-updated-pages">
 | 
			
		||||
                    @include('partials/entity-list', ['entities' => $recentlyUpdatedPages, 'style' => 'compact'])
 | 
			
		||||
                    @include('partials/entity-list', [
 | 
			
		||||
                    'entities' => $recentlyUpdatedPages,
 | 
			
		||||
                    'style' => 'compact',
 | 
			
		||||
                    'emptyText' => 'No pages have been recently updated'
 | 
			
		||||
                    ])
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,12 +10,12 @@
 | 
			
		|||
        <h4>Page Tags</h4>
 | 
			
		||||
        <div class="padded tags">
 | 
			
		||||
            <p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
 | 
			
		||||
            <table class="no-style" autosuggestions style="width: 100%;">
 | 
			
		||||
            <table class="no-style" tag-autosuggestions style="width: 100%;">
 | 
			
		||||
                <tbody ui-sortable="sortOptions" ng-model="tags" >
 | 
			
		||||
                    <tr ng-repeat="tag in tags track by $index">
 | 
			
		||||
                        <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
 | 
			
		||||
                        <td><input autosuggest="/ajax/tags/suggest/names" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
 | 
			
		||||
                        <td><input autosuggest="/ajax/tags/suggest/values" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
 | 
			
		||||
                        <td><input autosuggest="/ajax/tags/suggest/names" autosuggest-type="name" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
 | 
			
		||||
                        <td><input autosuggest="/ajax/tags/suggest/values" autosuggest-type="value" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
 | 
			
		||||
                        <td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,7 +61,7 @@
 | 
			
		|||
                            <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <textarea markdown-input md-change="editorChange" md-model="editContent"  name="markdown" rows="5"
 | 
			
		||||
                    <textarea markdown-input md-change="editorChange" id="markdown-editor-input" md-model="editContent"  name="markdown" rows="5"
 | 
			
		||||
                              @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
<div class="page {{$page->draft ? 'draft' : ''}}">
 | 
			
		||||
<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>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,11 +11,11 @@
 | 
			
		|||
 | 
			
		||||
    @if(isset($style) && $style === 'detailed')
 | 
			
		||||
        <div class="row meta text-muted text-small">
 | 
			
		||||
            <div class="col-md-4">
 | 
			
		||||
            <div class="col-md-6">
 | 
			
		||||
                Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif <br>
 | 
			
		||||
                Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-md-8">
 | 
			
		||||
            <div class="col-md-6">
 | 
			
		||||
                <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a>
 | 
			
		||||
                <br>
 | 
			
		||||
                @if($page->chapter)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
@extends('base')
 | 
			
		||||
 | 
			
		||||
@section('content')
 | 
			
		||||
 | 
			
		||||
    <div class="faded-small toolbar">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-sm-12 faded">
 | 
			
		||||
                    <div class="breadcrumbs">
 | 
			
		||||
                        <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
 | 
			
		||||
                        @if($page->hasChapter())
 | 
			
		||||
                            <span class="sep">»</span>
 | 
			
		||||
                            <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
 | 
			
		||||
                                <i class="zmdi zmdi-collection-bookmark"></i>
 | 
			
		||||
                                {{$page->chapter->getShortName()}}
 | 
			
		||||
                            </a>
 | 
			
		||||
                        @endif
 | 
			
		||||
                        <span class="sep">»</span>
 | 
			
		||||
                        <a href="{{$page->getUrl()}}" class="text-page text-button"><i class="zmdi zmdi-file-text"></i>{{ $page->getShortName() }}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <h1>Move Page <small class="subheader">{{$page->name}}</small></h1>
 | 
			
		||||
 | 
			
		||||
        <form action="{{ $page->getUrl() }}/move" method="POST">
 | 
			
		||||
            {!! csrf_field() !!}
 | 
			
		||||
            <input type="hidden" name="_method" value="PUT">
 | 
			
		||||
 | 
			
		||||
            @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter'])
 | 
			
		||||
 | 
			
		||||
            <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a>
 | 
			
		||||
            <button type="submit" class="button pos">Move Page</button>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			@ -28,15 +28,26 @@
 | 
			
		|||
                            </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>
 | 
			
		||||
                        @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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,4 @@
 | 
			
		|||
 | 
			
		||||
{{--Requires an entity to be passed with the name $entity--}}
 | 
			
		||||
 | 
			
		||||
@if(count($activity) > 0)
 | 
			
		||||
    <div class="activity-list">
 | 
			
		||||
        @foreach($activity as $activityItem)
 | 
			
		||||
| 
						 | 
				
			
			@ -10,5 +8,5 @@
 | 
			
		|||
        @endforeach
 | 
			
		||||
    </div>
 | 
			
		||||
@else
 | 
			
		||||
    <p class="text-muted">New activity will show up here.</p>
 | 
			
		||||
    <p class="text-muted">No activity to show</p>
 | 
			
		||||
@endif
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +1,20 @@
 | 
			
		|||
@if(Setting::get('app-color'))
 | 
			
		||||
    <style>
 | 
			
		||||
        header, #back-to-top, .primary-background {
 | 
			
		||||
            background-color: {{ Setting::get('app-color') }};
 | 
			
		||||
        }
 | 
			
		||||
        .faded-small, .primary-background-light {
 | 
			
		||||
            background-color: {{ Setting::get('app-color-light') }};
 | 
			
		||||
        }
 | 
			
		||||
        .button-base, .button, input[type="button"], input[type="submit"] {
 | 
			
		||||
            background-color: {{ Setting::get('app-color') }};
 | 
			
		||||
        }
 | 
			
		||||
        .button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
 | 
			
		||||
            background-color: {{ Setting::get('app-color') }};
 | 
			
		||||
        }
 | 
			
		||||
        .nav-tabs a.selected, .nav-tabs .tab-item.selected {
 | 
			
		||||
            border-bottom-color: {{ Setting::get('app-color') }};
 | 
			
		||||
        }
 | 
			
		||||
        p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
 | 
			
		||||
            color: {{ Setting::get('app-color') }};
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
@endif
 | 
			
		||||
<style>
 | 
			
		||||
    header, #back-to-top, .primary-background {
 | 
			
		||||
        background-color: {{ Setting::get('app-color') }} !important;
 | 
			
		||||
    }
 | 
			
		||||
    .faded-small, .primary-background-light {
 | 
			
		||||
        background-color: {{ Setting::get('app-color-light') }};
 | 
			
		||||
    }
 | 
			
		||||
    .button-base, .button, input[type="button"], input[type="submit"] {
 | 
			
		||||
        background-color: {{ Setting::get('app-color') }};
 | 
			
		||||
    }
 | 
			
		||||
    .button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
 | 
			
		||||
        background-color: {{ Setting::get('app-color') }};
 | 
			
		||||
    }
 | 
			
		||||
    .nav-tabs a.selected, .nav-tabs .tab-item.selected {
 | 
			
		||||
        border-bottom-color: {{ Setting::get('app-color') }};
 | 
			
		||||
    }
 | 
			
		||||
    p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
 | 
			
		||||
        color: {{ Setting::get('app-color') }};
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -16,8 +16,8 @@
 | 
			
		|||
 | 
			
		||||
        @endforeach
 | 
			
		||||
    @else
 | 
			
		||||
        <p class="text-muted">
 | 
			
		||||
            No items available
 | 
			
		||||
        <p class="text-muted empty-text">
 | 
			
		||||
            {{ $emptyText or 'No items available' }}
 | 
			
		||||
        </p>
 | 
			
		||||
    @endif
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<div class="form-group">
 | 
			
		||||
    <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
 | 
			
		||||
        <input type="hidden" entity-selector-input name="{{$name}}" value="">
 | 
			
		||||
        <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>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
 | 
			
		||||
<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif>
 | 
			
		||||
    <i class="zmdi zmdi-check-circle"></i> <span>{!! nl2br(htmlentities(Session::get('success'))) !!}</span>
 | 
			
		||||
<div class="notification anim pos" @if(!session()->has('success')) style="display:none;" @endif>
 | 
			
		||||
    <i class="zmdi zmdi-check-circle"></i> <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="notification anim warning stopped" @if(!Session::has('warning')) style="display:none;" @endif>
 | 
			
		||||
    <i class="zmdi zmdi-info"></i> <span>{!! nl2br(htmlentities(Session::get('warning'))) !!}</span>
 | 
			
		||||
<div class="notification anim warning stopped" @if(!session()->has('warning')) style="display:none;" @endif>
 | 
			
		||||
    <i class="zmdi zmdi-info"></i> <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif>
 | 
			
		||||
    <i class="zmdi zmdi-alert-circle"></i> <span>{!! nl2br(htmlentities(Session::get('error'))) !!}</span>
 | 
			
		||||
<div class="notification anim neg stopped" @if(!session()->has('error')) style="display:none;" @endif>
 | 
			
		||||
    <i class="zmdi zmdi-alert-circle"></i> <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<div class="entity-list @if(isset($style)){{ $style }}@endif" ng-non-bindable>
 | 
			
		||||
    @if(count($entities) > 0)
 | 
			
		||||
        @foreach($entities as $index => $entity)
 | 
			
		||||
            @if($entity->isA('page'))
 | 
			
		||||
                @include('pages/list-item', ['page' => $entity])
 | 
			
		||||
            @elseif($entity->isA('book'))
 | 
			
		||||
                @include('books/list-item', ['book' => $entity])
 | 
			
		||||
            @elseif($entity->isA('chapter'))
 | 
			
		||||
                @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true, 'showPath' => true])
 | 
			
		||||
            @endif
 | 
			
		||||
 | 
			
		||||
            @if($index !== count($entities) - 1)
 | 
			
		||||
                <hr>
 | 
			
		||||
            @endif
 | 
			
		||||
 | 
			
		||||
        @endforeach
 | 
			
		||||
    @else
 | 
			
		||||
        <p class="text-muted">
 | 
			
		||||
            No items available
 | 
			
		||||
        </p>
 | 
			
		||||
    @endif
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -82,4 +82,14 @@ class EntitySearchTest extends TestCase
 | 
			
		|||
        $this->asAdmin()->visit('/search/books?term=' . $book->name)
 | 
			
		||||
            ->see('Book Search Results')->see('.entity-list', $book->name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_ajax_entity_search()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Page::all()->last();
 | 
			
		||||
        $notVisitedPage = \BookStack\Page::first();
 | 
			
		||||
        $this->visit($page->getUrl());
 | 
			
		||||
        $this->asAdmin()->visit('/ajax/search/entities?term=' . $page->name)->see('.entity-list', $page->name);
 | 
			
		||||
        $this->asAdmin()->visit('/ajax/search/entities?types=book&term=' . $page->name)->dontSee('.entity-list', $page->name);
 | 
			
		||||
        $this->asAdmin()->visit('/ajax/search/entities')->see('.entity-list', $page->name)->dontSee($notVisitedPage->name);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,4 +22,47 @@ class SortTest extends TestCase
 | 
			
		|||
            ->dontSee($draft->name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_page_move()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Page::first();
 | 
			
		||||
        $currentBook = $page->book;
 | 
			
		||||
        $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
 | 
			
		||||
        $this->asAdmin()->visit($page->getUrl() . '/move')
 | 
			
		||||
            ->see('Move Page')->see($page->name)
 | 
			
		||||
            ->type('book:' . $newBook->id, 'entity_selection')->press('Move Page');
 | 
			
		||||
 | 
			
		||||
        $page = \BookStack\Page::find($page->id);
 | 
			
		||||
        $this->seePageIs($page->getUrl());
 | 
			
		||||
        $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
 | 
			
		||||
 | 
			
		||||
        $this->visit($newBook->getUrl())
 | 
			
		||||
            ->seeInNthElement('.activity-list-item', 0, 'moved page')
 | 
			
		||||
            ->seeInNthElement('.activity-list-item', 0, $page->name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_chapter_move()
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = \BookStack\Chapter::first();
 | 
			
		||||
        $currentBook = $chapter->book;
 | 
			
		||||
        $pageToCheck = $chapter->pages->first();
 | 
			
		||||
        $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
 | 
			
		||||
 | 
			
		||||
        $this->asAdmin()->visit($chapter->getUrl() . '/move')
 | 
			
		||||
            ->see('Move Chapter')->see($chapter->name)
 | 
			
		||||
            ->type('book:' . $newBook->id, 'entity_selection')->press('Move Chapter');
 | 
			
		||||
 | 
			
		||||
        $chapter = \BookStack\Chapter::find($chapter->id);
 | 
			
		||||
        $this->seePageIs($chapter->getUrl());
 | 
			
		||||
        $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');
 | 
			
		||||
 | 
			
		||||
        $this->visit($newBook->getUrl())
 | 
			
		||||
            ->seeInNthElement('.activity-list-item', 0, 'moved chapter')
 | 
			
		||||
            ->seeInNthElement('.activity-list-item', 0, $chapter->name);
 | 
			
		||||
 | 
			
		||||
        $pageToCheck = \BookStack\Page::find($pageToCheck->id);
 | 
			
		||||
        $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book');
 | 
			
		||||
        $this->visit($pageToCheck->getUrl())
 | 
			
		||||
            ->see($newBook->name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
class ImageTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a test image that can be uploaded
 | 
			
		||||
     * @param $fileName
 | 
			
		||||
     * @return \Illuminate\Http\UploadedFile
 | 
			
		||||
     */
 | 
			
		||||
    protected function getTestImage($fileName)
 | 
			
		||||
    {
 | 
			
		||||
        return new \Illuminate\Http\UploadedFile(base_path('tests/test-image.jpg'), $fileName, 'image/jpeg', 5238);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the path for a test image.
 | 
			
		||||
     * @param $type
 | 
			
		||||
     * @param $fileName
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function getTestImagePath($type, $fileName)
 | 
			
		||||
    {
 | 
			
		||||
        return '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/' . $fileName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Uploads an image with the given name.
 | 
			
		||||
     * @param $name
 | 
			
		||||
     * @param int $uploadedTo
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function uploadImage($name, $uploadedTo = 0)
 | 
			
		||||
    {
 | 
			
		||||
        $file = $this->getTestImage($name);
 | 
			
		||||
        $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
 | 
			
		||||
        return $this->getTestImagePath('gallery', $name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete an uploaded image.
 | 
			
		||||
     * @param $relPath
 | 
			
		||||
     */
 | 
			
		||||
    protected function deleteImage($relPath)
 | 
			
		||||
    {
 | 
			
		||||
        unlink(public_path($relPath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function test_image_upload()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Page::first();
 | 
			
		||||
        $this->asAdmin();
 | 
			
		||||
        $admin = $this->getAdmin();
 | 
			
		||||
        $imageName = 'first-image.jpg';
 | 
			
		||||
 | 
			
		||||
        $relPath = $this->uploadImage($imageName, $page->id);
 | 
			
		||||
        $this->assertResponseOk();
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists');
 | 
			
		||||
 | 
			
		||||
        $this->seeInDatabase('images', [
 | 
			
		||||
            'url' => $relPath,
 | 
			
		||||
            'type' => 'gallery',
 | 
			
		||||
            'uploaded_to' => $page->id,
 | 
			
		||||
            'path' => $relPath,
 | 
			
		||||
            'created_by' => $admin->id,
 | 
			
		||||
            'updated_by' => $admin->id,
 | 
			
		||||
            'name' => $imageName
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        $this->deleteImage($relPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_image_delete()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Page::first();
 | 
			
		||||
        $this->asAdmin();
 | 
			
		||||
        $imageName = 'first-image.jpg';
 | 
			
		||||
 | 
			
		||||
        $relPath = $this->uploadImage($imageName, $page->id);
 | 
			
		||||
        $image = \BookStack\Image::first();
 | 
			
		||||
 | 
			
		||||
        $this->call('DELETE', '/images/' . $image->id);
 | 
			
		||||
        $this->assertResponseOk();
 | 
			
		||||
 | 
			
		||||
        $this->dontSeeInDatabase('images', [
 | 
			
		||||
            'url' => $relPath,
 | 
			
		||||
            'type' => 'gallery'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has been deleted');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -39,11 +39,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
 | 
			
		|||
     */
 | 
			
		||||
    public function asAdmin()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->actingAs($this->getAdmin());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the current admin user.
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getAdmin() {
 | 
			
		||||
        if($this->admin === null) {
 | 
			
		||||
            $adminRole = \BookStack\Role::getRole('admin');
 | 
			
		||||
            $this->admin = $adminRole->users->first();
 | 
			
		||||
        }
 | 
			
		||||
        return $this->actingAs($this->admin);
 | 
			
		||||
        return $this->admin;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.1 KiB  | 
		Loading…
	
		Reference in New Issue