]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'master' into 2019-design
authorDan Brown <redacted>
Sun, 10 Mar 2019 21:40:02 +0000 (21:40 +0000)
committerDan Brown <redacted>
Sun, 10 Mar 2019 21:40:02 +0000 (21:40 +0000)
1  2 
app/Http/Controllers/PageController.php
app/Settings/SettingService.php
app/helpers.php
resources/assets/js/components/wysiwyg-editor.js
resources/assets/sass/_pages.scss
resources/views/common/header.blade.php
resources/views/pages/show.blade.php
resources/views/users/edit.blade.php

index 7ebf262097a46d06b68ed9c28335e3bf177d11ab,d95e02470bee10c63d436421ac7a53cdcff6577c..16a7d5a5e45df6df1094bfa14df63fb17cb278f3
@@@ -61,7 -61,7 +61,7 @@@ class PageController extends Controlle
  
          // Otherwise show the edit view if they're a guest
          $this->setPageTitle(trans('entities.pages_new'));
 -        return view('pages/guest-create', ['parent' => $parent]);
 +        return view('pages.guest-create', ['parent' => $parent]);
      }
  
      /**
          $this->setPageTitle(trans('entities.pages_edit_draft'));
  
          $draftsEnabled = $this->signedIn;
 -        return view('pages/edit', [
 +        return view('pages.edit', [
              'page' => $draft,
              'book' => $draft->book,
              'isDraft' => true,
  
          Views::add($page);
          $this->setPageTitle($page->getShortName());
 -        return view('pages/show', [
 +        return view('pages.show', [
              'page' => $page,'book' => $page->book,
              'current' => $page,
              'sidebarTree' => $sidebarTree,
          }
  
          $draftsEnabled = $this->signedIn;
 -        return view('pages/edit', [
 +        return view('pages.edit', [
              'page' => $page,
              'book' => $page->book,
              'current' => $page,
          $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
          $this->checkOwnablePermission('page-delete', $page);
          $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
 -        return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
 +        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
      }
  
  
          $page = $this->pageRepo->getById('page', $pageId, true);
          $this->checkOwnablePermission('page-update', $page);
          $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
 -        return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
 +        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
      }
  
      /**
       * @param string $bookSlug
       * @param string $pageSlug
       * @return \Illuminate\View\View
 +     * @throws NotFoundException
       */
      public function showRevisions($bookSlug, $pageSlug)
      {
          $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
          $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
 -        return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
 +        return view('pages.revisions', ['page' => $page, 'current' => $page]);
      }
  
      /**
          $page->fill($revision->toArray());
          $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
  
 -        return view('pages/revision', [
 +        return view('pages.revision', [
              'page' => $page,
              'book' => $page->book,
 +            'diff' => null,
              'revision' => $revision
          ]);
      }
          $page->fill($revision->toArray());
          $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
  
 -        return view('pages/revision', [
 +        return view('pages.revision', [
              'page' => $page,
              'book' => $page->book,
              'diff' => $diff,
          // Check if its the latest revision, cannot delete latest revision.
          if (intval($currentRevision->id) === intval($revId)) {
              session()->flash('error', trans('entities.revision_cannot_delete_latest'));
 -            return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
 +            return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
          }
  
          $revision->delete();
          session()->flash('success', trans('entities.revision_delete_success'));
 -        return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
 +        return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
      }
  
      /**
          return $this->downloadResponse($pageText, $pageSlug . '.txt');
      }
  
 -    /**
 -     * Show a listing of recently created pages
 -     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 -     */
 -    public function showRecentlyCreated()
 -    {
 -        $pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
 -        return view('pages/detailed-listing', [
 -            'title' => trans('entities.recently_created_pages'),
 -            'pages' => $pages
 -        ]);
 -    }
 -
      /**
       * Show a listing of recently created pages
       * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
       */
      public function showRecentlyUpdated()
      {
 +        // TODO - Still exist?
          $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
 -        return view('pages/detailed-listing', [
 +        return view('pages.detailed-listing', [
              'title' => trans('entities.recently_updated_pages'),
              'pages' => $pages
          ]);
      }
  
 -    /**
 -     * Show the Restrictions view.
 -     * @param string $bookSlug
 -     * @param string $pageSlug
 -     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 -     */
 -    public function showRestrict($bookSlug, $pageSlug)
 -    {
 -        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
 -        $this->checkOwnablePermission('restrictions-manage', $page);
 -        $roles = $this->userRepo->getRestrictableRoles();
 -        return view('pages/restrictions', [
 -            'page'  => $page,
 -            'roles' => $roles
 -        ]);
 -    }
 -
      /**
       * Show the view to choose a new parent to move a page into.
       * @param string $bookSlug
          $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
          $this->checkOwnablePermission('page-update', $page);
          $this->checkOwnablePermission('page-delete', $page);
 -        return view('pages/move', [
 +        return view('pages.move', [
              'book' => $page->book,
              'page' => $page
          ]);
      public function showCopy($bookSlug, $pageSlug)
      {
          $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-         $this->checkOwnablePermission('page-update', $page);
+         $this->checkOwnablePermission('page-view', $page);
          session()->flashInput(['name' => $page->name]);
 -        return view('pages/copy', [
 +        return view('pages.copy', [
              'book' => $page->book,
              'page' => $page
          ]);
      public function copy($bookSlug, $pageSlug, Request $request)
      {
          $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-         $this->checkOwnablePermission('page-update', $page);
+         $this->checkOwnablePermission('page-view', $page);
  
          $entitySelection = $request->get('entity_selection', null);
          if ($entitySelection === null || $entitySelection === '') {
          return redirect($pageCopy->getUrl());
      }
  
 +    /**
 +     * Show the Permissions view.
 +     * @param string $bookSlug
 +     * @param string $pageSlug
 +     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 +     * @throws NotFoundException
 +     */
 +    public function showPermissions($bookSlug, $pageSlug)
 +    {
 +        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
 +        $this->checkOwnablePermission('restrictions-manage', $page);
 +        $roles = $this->userRepo->getRestrictableRoles();
 +        return view('pages.permissions', [
 +            'page'  => $page,
 +            'roles' => $roles
 +        ]);
 +    }
 +
      /**
       * Set the permissions for this page.
       * @param string $bookSlug
       * @param Request $request
       * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
       * @throws NotFoundException
 +     * @throws \Throwable
       */
 -    public function restrict($bookSlug, $pageSlug, Request $request)
 +    public function permissions($bookSlug, $pageSlug, Request $request)
      {
          $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
          $this->checkOwnablePermission('restrictions-manage', $page);
index 42a3810608750c9afbdf4e3352c29350d9511d4d,c9491e3eeb4b4c949248680b9d43b1a0e45c3f59..663a6ae3275954a2fa944c3d80fa07d832861038
@@@ -41,6 -41,7 +41,7 @@@ class SettingServic
          if ($default === false) {
              $default = config('setting-defaults.' . $key, false);
          }
          if (isset($this->localCache[$key])) {
              return $this->localCache[$key];
          }
@@@ -60,9 -61,6 +61,9 @@@
       */
      public function getUser($user, $key, $default = false)
      {
 +        if ($user->isDefault()) {
 +            return session()->get($key, $default);
 +        }
          return $this->get($this->userKey($user->id, $key), $default);
      }
  
       */
      public function putUser($user, $key, $value)
      {
 +        if ($user->isDefault()) {
 +            return session()->put($key, $value);
 +        }
          return $this->put($this->userKey($user->id, $key), $value);
      }
  
diff --combined app/helpers.php
index e1395d816e987ee468c2af849478a04ebc0a907d,d9533645de6e31ae1c464b7466f1ee71a498a721..3f7b5e1b1350c1294adfee1b001775fb3a72b62b
@@@ -1,5 -1,7 +1,7 @@@
  <?php
  
+ use BookStack\Auth\Permissions\PermissionService;
+ use BookStack\Entities\Entity;
  use BookStack\Ownable;
  
  /**
@@@ -41,38 -43,43 +43,51 @@@ function user(
   * Check if current user is a signed in user.
   * @return bool
   */
 -function signedInUser()
 +function signedInUser() : bool
  {
      return auth()->user() && !auth()->user()->isDefault();
  }
  
 +/**
 + * Check if the current user has general access.
 + * @return bool
 + */
 +function hasAppAccess() : bool {
 +    return !auth()->guest() || setting('app-public');
 +}
 +
  /**
   * Check if the current user has a permission.
   * If an ownable element is passed in the jointPermissions are checked against
   * that particular item.
-  * @param $permission
+  * @param string $permission
   * @param Ownable $ownable
   * @return mixed
   */
- function userCan($permission, Ownable $ownable = null)
+ function userCan(string $permission, Ownable $ownable = null)
  {
      if ($ownable === null) {
          return user() && user()->can($permission);
      }
  
      // Check permission on ownable item
-     $permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
+     $permissionService = app(PermissionService::class);
      return $permissionService->checkOwnableUserAccess($ownable, $permission);
  }
  
+ /**
+  * Check if the current user has the given permission
+  * on any item in the system.
+  * @param string $permission
+  * @param string|null $entityClass
+  * @return bool
+  */
+ function userCanOnAny(string $permission, string $entityClass = null)
+ {
+     $permissionService = app(PermissionService::class);
+     return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
+ }
  /**
   * Helper to access system settings.
   * @param $key
index c70d8271986156c30c2366a2c902a42431076d3d,9deb1d0a7a0f4e2a9c16945d919fe6e9b61148ff..2767d35c0f80cdf4a68af9a4ad3924080a3b7b89
@@@ -4,22 -4,24 +4,24 @@@ import DrawIO from "../services/drawio"
  /**
   * Handle pasting images from clipboard.
   * @param {ClipboardEvent} event
+  * @param {WysiwygEditor} wysiwygComponent
   * @param editor
   */
- function editorPaste(event, editor) {
+ function editorPaste(event, editor, wysiwygComponent) {
      if (!event.clipboardData || !event.clipboardData.items) return;
-     let items = event.clipboardData.items;
  
-     for (let i = 0; i < items.length; i++) {
-         if (items[i].type.indexOf("image") === -1) continue;
+     for (let clipboardItem of event.clipboardData.items) {
+         if (clipboardItem.type.indexOf("image") === -1) continue;
          event.preventDefault();
  
-         let id = "image-" + Math.random().toString(16).slice(2);
-         let loadingImage = window.baseUrl('/loading.gif');
-         let file = items[i].getAsFile();
+         const id = "image-" + Math.random().toString(16).slice(2);
+         const loadingImage = window.baseUrl('/loading.gif');
+         const file = clipboardItem.getAsFile();
          setTimeout(() => {
              editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
-             uploadImageFile(file).then(resp => {
+             uploadImageFile(file, wysiwygComponent).then(resp => {
                  editor.dom.setAttrib(id, 'src', resp.thumbs.display);
              }).catch(err => {
                  editor.dom.remove(id);
  /**
   * Upload an image file to the server
   * @param {File} file
+  * @param {WysiwygEditor} wysiwygComponent
   */
- function uploadImageFile(file) {
-     if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`);
+ async function uploadImageFile(file, wysiwygComponent) {
+     if (file === null || file.type.indexOf('image') !== 0) {
+         throw new Error(`Not an image file`);
+     }
  
      let ext = 'png';
      if (file.name) {
          if (fileNameMatches.length > 1) ext = fileNameMatches[1];
      }
  
-     let remoteFilename = "image-" + Date.now() + "." + ext;
-     let formData = new FormData();
+     const remoteFilename = "image-" + Date.now() + "." + ext;
+     const formData = new FormData();
      formData.append('file', file, remoteFilename);
+     formData.append('uploaded_to', wysiwygComponent.pageId);
  
-     return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
+     const resp = await window.$http.post(window.baseUrl('/images/gallery/upload'), formData);
+     return resp.data;
  }
  
  function registerEditorShortcuts(editor) {
@@@ -370,7 -377,10 +377,10 @@@ class WysiwygEditor 
  
      constructor(elem) {
          this.elem = elem;
-         this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
+         const pageEditor = document.getElementById('page-editor');
+         this.pageId = pageEditor.getAttribute('page-id');
+         this.textDirection = pageEditor.getAttribute('text-direction');
  
          this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media";
          this.loadPlugins();
      }
  
      getTinyMceConfig() {
+         const context = this;
          return {
              selector: '#html-editor',
              content_css: [
              plugins: this.plugins,
              imagetools_toolbar: 'imageoptions',
              toolbar: this.getToolBar(),
 -            content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
 +            content_style: "html, body {background: #FFF;} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
              style_formats: [
                  {title: "Header Large", format: "h2"},
                  {title: "Header Medium", format: "h3"},
                  });
  
                  // Paste image-uploads
-                 editor.on('paste', event => editorPaste(event, editor));
+                 editor.on('paste', event => editorPaste(event, editor, context));
              }
          };
      }
index 3ab5a6a691e112f602e8311ed98f07488a7b55da,84280319debff1d04fd30d4633ca0448b66d6399..1b11930796c569e033f5a1a66d422621aa72c336
@@@ -3,7 -3,6 +3,7 @@@
    flex-direction: column;
    align-items: stretch;
    overflow: hidden;
 +  background-color: #FFF;
    .faded-small {
      height: auto;
    }
    width: 100%;
    max-width: 840px;
    margin: 0 auto;
 -  margin-top: $-xxl;
    overflow-wrap: break-word;
 -  &.flex {
 -    margin-top: $-m;
 -  }
    .align-left {
      text-align: left;
    }
      margin: $-xs $-s $-xs 0;
    }
    .align-right {
-     float: right !important;
+     text-align: right !important;
    }
    img.align-right, table.align-right {
-     text-align: right;
+     float: right !important;
      margin: $-xs 0 $-xs $-s;
    }
    .align-center {
      text-align: center;
    }
+   img.align-center {
+     display: block;
+   }
+   img.align-center, table.align-center {
+     margin-left: auto;
+     margin-right: auto;
+   }
    img {
      max-width: 100%;
      height:auto;
    }
  }
  
 -.comments-container {
 -  width: 100%;
 -  border-top: 1px solid #DDD;
 -  margin-top: $-xl;
 -  margin-bottom: $-m;
 -  h5 {
 -    color: #888;
 -    font-weight: normal;
 -    margin-top: 0.5em;
 -  }
 +.comments-container h5 {
 +  color: #888;
 +  font-weight: normal;
 +  margin-top: 0.5em;
  }
  
  .comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
    .mce-open {
      display: none;
    }
 +}
 +
 +.entity-list-item > span:first-child, .icon-list-item > span:first-child {
 +  font-size: 0.8rem;
 +  width: 1.88em;
 +  height: 1.88em;
 +  display: flex;
 +  align-items: center;
 +  justify-content: center;
 +  text-align: center;
 +  border-radius: 1em;
 +  position: relative;
 +  overflow: hidden;
 +  svg {
 +    margin: 0;
 +    bottom: 0;
 +  }
 +  &:after {
 +    content: '';
 +    position: absolute;
 +    background-color: currentColor;
 +    opacity: 0.2;
 +    left: 0;
 +    top: 0;
 +    width: 100%;
 +    height: 100%;
 +  }
 +}
 +
 +.entity-chip {
 +  display: inline-block;
 +  align-items: center;
 +  justify-content: center;
 +  text-align: center;
 +  font-size: 0.9em;
 +  border-radius: 3px;
 +  position: relative;
 +  overflow: hidden;
 +  padding: $-xs $-s;
 +  fill: currentColor;
 +  opacity: 0.85;
 +  transition: opacity ease-in-out 120ms;
 +  &:after {
 +    content: '';
 +    position: absolute;
 +    background-color: currentColor;
 +    opacity: 0.15;
 +    left: 0;
 +    top: 0;
 +    width: 100%;
 +    height: 100%;
 +  }
 +  &:hover {
 +    text-decoration: none;
 +    opacity: 1;
 +  }
  }
index f1661a14600bc97ef02e5ae3b5a1e7cfb32b2c2f,0000000000000000000000000000000000000000..89aa1078d865d149f21de14c9621d69db5f06843
mode 100644,000000..100644
--- /dev/null
@@@ -1,73 -1,0 +1,73 @@@
-                         @if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
 +<header id="header" header-mobile-toggle>
 +    <div class="grid break-l mx-l">
 +
 +        <div>
 +            <a href="{{ baseUrl('/') }}" class="logo">
 +                @if(setting('app-logo', '') !== 'none')
 +                    <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
 +                @endif
 +                @if (setting('app-name-header'))
 +                    <span class="logo-text">{{ setting('app-name') }}</span>
 +                @endif
 +            </a>
 +            <div class="mobile-menu-toggle hide-over-l">@icon('more')</div>
 +        </div>
 +
 +        <div class="header-search hide-under-l">
 +            @if (hasAppAccess())
 +            <form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
 +                <button id="header-search-box-button" type="submit">@icon('search') </button>
 +                <input id="header-search-box-input" type="text" name="term" tabindex="2" placeholder="{{ trans('common.search') }}" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
 +            </form>
 +            @endif
 +        </div>
 +
 +        <div class="text-right">
 +            <div class="header-links">
 +                <div class="links text-center">
 +                    @if (hasAppAccess())
 +                        <a class="hide-over-l" href="{{ baseUrl('/search') }}">@icon('search'){{ trans('common.search') }}</a>
++                        @if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
 +                            <a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
 +                        @endif
 +                        <a href="{{ baseUrl('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
 +                        @if(signedInUser() && userCan('settings-manage'))
 +                            <a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
 +                        @endif
 +                        @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
 +                            <a href="{{ baseUrl('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
 +                        @endif
 +                    @endif
 +
 +                    @if(!signedInUser())
 +                        @if(setting('registration-enabled', false))
 +                            <a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
 +                        @endif
 +                        <a href="{{ baseUrl('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
 +                    @endif
 +                </div>
 +                @if(signedInUser())
 +                    <?php $currentUser = user(); ?>
 +                    <div class="dropdown-container" dropdown>
 +                        <span class="user-name hide-under-l" dropdown-toggle>
 +                            <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
 +                            <span class="name">{{ $currentUser->getShortName(9) }}</span> @icon('caret-down')
 +                        </span>
 +                        <ul>
 +                            <li>
 +                                <a href="{{ baseUrl("/user/{$currentUser->id}") }}" class="text-primary">@icon('user'){{ trans('common.view_profile') }}</a>
 +                            </li>
 +                            <li>
 +                                <a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}" class="text-primary">@icon('edit'){{ trans('common.edit_profile') }}</a>
 +                            </li>
 +                            <li>
 +                                <a href="{{ baseUrl('/logout') }}" class="text-neg">@icon('logout'){{ trans('auth.logout') }}</a>
 +                            </li>
 +                        </ul>
 +                    </div>
 +                @endif
 +            </div>
 +        </div>
 +
 +    </div>
 +</header>
index 8444155a69a7984dde04de4ba9a548be23e352e9,db2f1462e3d05bedbea27fef66c9677c5efc599c..6858661c412b2d6af3f9d6bc1c9584ad8332b3da
@@@ -1,45 -1,50 +1,45 @@@
 -@extends('sidebar-layout')
 +@extends('tri-layout')
  
 -@section('toolbar')
 -    <div class="col-sm-8 col-xs-5 faded">
 -        @include('pages._breadcrumbs', ['page' => $page])
 +@section('body')
 +
 +    <div class="mb-m">
 +        @include('partials.breadcrumbs', ['crumbs' => [
 +            $page->book,
 +            $page->hasChapter() ? $page->chapter : null,
 +            $page,
 +        ]])
      </div>
 -    <div class="col-sm-4 col-xs-7 faded">
 -        <div class="action-buttons">
 -            <span dropdown class="dropdown-container">
 -                <div dropdown-toggle class="text-button text-primary">@icon('export'){{ trans('entities.export') }}</div>
 -                <ul class="wide">
 -                    <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
 -                    <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
 -                    <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
 -                </ul>
 -            </span>
 -            @if(userCan('page-update', $page))
 -                <a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" >@icon('edit'){{ trans('common.edit') }}</a>
 -            @endif
 -            @if((userCan('page-view', $page) && userCanOnAny('page-create')) || 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">@icon('more') {{ trans('common.more') }}</a>
 -                    <ul>
 -                        @if(userCanOnAny('page-create'))
 -                            <li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
 -                        @endif
 -                        @if(userCan('page-delete', $page) && userCan('page-update', $page))
 -                            <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
 -                        @endif
 -                        @if(userCan('page-update', $page))
 -                            <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
 -                        @endif
 -                        @if(userCan('restrictions-manage', $page))
 -                            <li><a href="{{ $page->getUrl('/permissions') }}" class="text-primary">@icon('lock'){{ trans('entities.permissions') }}</a></li>
 -                        @endif
 -                        @if(userCan('page-delete', $page))
 -                            <li><a href="{{ $page->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('common.delete') }}</a></li>
 -                        @endif
 -                    </ul>
 +
 +    <div class="content-wrap card">
 +        <div class="page-content flex" page-display="{{ $page->id }}">
 +
 +            <div class="pointer-container" id="pointer">
 +                <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
 +                    <span class="icon text-primary">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
 +                    <span class="input-group">
 +                    <input readonly="readonly" type="text" id="pointer-url" placeholder="url">
 +                    <button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
 +                </span>
 +                    @if(userCan('page-update', $page))
 +                        <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
 +                           class="button icon heading-edit-icon" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
 +                    @endif
                  </div>
 -            @endif
 +            </div>
  
 +            @include('pages.page-display')
          </div>
      </div>
 +
 +    @if ($commentsEnabled)
 +        <div class="container small nopad comments-container mb-l">
 +            @include('comments.comments', ['page' => $page])
 +            <div class="clearfix"></div>
 +        </div>
 +    @endif
  @stop
  
 -@section('sidebar')
 +@section('left')
  
      @if($page->tags->count() > 0)
          <section>
@@@ -48,8 -53,8 +48,8 @@@
      @endif
  
      @if ($page->attachments->count() > 0)
 -        <div class="card">
 -            <h3>@icon('attach') {{ trans('entities.pages_attachments') }}</h3>
 +        <div id="page-attachments" class="mb-xl">
 +            <h5>{{ trans('entities.pages_attachments') }}</h5>
              <div class="body">
                  @foreach($page->attachments as $attachment)
                      <div class="attachment">
      @endif
  
      @if (isset($pageNav) && count($pageNav))
 -        <div class="card">
 -            <h3>@icon('open-book') {{ trans('entities.pages_navigation') }}</h3>
 +        <div id="page-navigation" class="mb-xl">
 +            <h5>{{ trans('entities.pages_navigation') }}</h5>
              <div class="body">
                  <div class="sidebar-page-nav menu">
                      @foreach($pageNav as $navItem)
                          <li class="page-nav-item h{{ $navItem['level'] }}">
                              <a href="{{ $navItem['link'] }}">{{ $navItem['text'] }}</a>
 +                            <div class="primary-background sidebar-page-nav-bullet"></div>
                          </li>
                      @endforeach
                  </div>
@@@ -76,8 -80,8 +76,8 @@@
          </div>
      @endif
  
 -    <div class="card entity-details">
 -        <h3>@icon('info') {{ trans('common.details') }}</h3>
 +    <div id="page-details" class="entity-details mb-xl">
 +        <h5>{{ trans('common.details') }}</h5>
          <div class="body text-muted text-small blended-links">
              @include('partials.entity-meta', ['entity' => $page])
  
          </div>
      </div>
  
 -    @include('partials/book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
 -
 +    @include('partials.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
  @stop
  
 -@section('body-wrap-classes', 'flex-fill columns')
 +@section('right')
 +    <div class="actions mb-xl">
 +        <h5>Actions</h5>
  
 -@section('body')
 -
 -    <div class="page-content flex" page-display="{{ $page->id }}">
 +        <div class="icon-list text-primary">
 +            {{--Export--}}
 +            <div dropdown class="dropdown-container block">
 +                <div dropdown-toggle class="icon-list-item">
 +                    <span>@icon('export')</span>
 +                    <span>{{ trans('entities.export') }}</span>
 +                </div>
 +                <ul class="wide">
 +                    <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
 +                    <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
 +                    <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
 +                </ul>
 +            </div>
  
 -        <div class="pointer-container" id="pointer">
 -            <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
 -                <span class="icon text-primary">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
 -                <span class="input-group">
 -                    <input readonly="readonly" type="text" id="pointer-url" placeholder="url">
 -                    <button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
 -                </span>
 -                @if(userCan('page-update', $page))
 -                    <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
 -                        class="button icon heading-edit-icon" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
 +            {{--User Actions--}}
 +            @if(userCan('page-update', $page))
 +                <a href="{{ $page->getUrl('/edit') }}" class="icon-list-item">
 +                    <span>@icon('edit')</span>
 +                    <span>{{ trans('common.edit') }}</span>
 +                </a>
++            @endif
++            @if(userCanOnAny('page-create'))
 +                <a href="{{ $page->getUrl('/copy') }}" class="icon-list-item">
 +                    <span>@icon('copy')</span>
 +                    <span>{{ trans('common.copy') }}</span>
 +                </a>
++            @endif
++            @if(userCan('page-update', $page))
 +                @if(userCan('page-delete', $page))
 +                      <a href="{{ $page->getUrl('/move') }}" class="icon-list-item">
 +                          <span>@icon('folder')</span>
 +                          <span>{{ trans('common.move') }}</span>
 +                      </a>
                  @endif
 -            </div>
 +                <a href="{{ $page->getUrl('/revisions') }}" class="icon-list-item">
 +                    <span>@icon('history')</span>
 +                    <span>{{ trans('entities.revisions') }}</span>
 +                </a>
 +            @endif
 +            @if(userCan('restrictions-manage', $page))
 +                <a href="{{ $page->getUrl('/permissions') }}" class="icon-list-item">
 +                    <span>@icon('lock')</span>
 +                    <span>{{ trans('entities.permissions') }}</span>
 +                </a>
 +            @endif
 +            @if(userCan('page-delete', $page))
 +                <a href="{{ $page->getUrl('/delete') }}" class="icon-list-item">
 +                    <span>@icon('delete')</span>
 +                    <span>{{ trans('common.delete') }}</span>
 +                </a>
 +            @endif
          </div>
  
 -        @include('pages/page-display')
      </div>
 -
 -    @if ($commentsEnabled)
 -      <div class="container small nopad comments-container">
 -          @include('comments/comments', ['page' => $page])
 -      </div>
 -    @endif
  @stop
index 42fc2beb1bba9135a82425d0657bb0a6c3a262fe,1b0514f9cdc4de07a3cfcee151e3abb62c2ff44e..e6e66665f08bb97e2d1e0edc95d1197ba011324a
@@@ -1,91 -1,91 +1,91 @@@
  @extends('simple-layout')
  
 -@section('toolbar')
 -    @include('settings/navbar', ['selected' => 'users'])
 -@stop
 -
  @section('body')
 -
      <div class="container small">
 -        <p>&nbsp;</p>
 -        <div class="card">
 -            <h3>@icon('edit') {{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h3>
 -            <div class="body">
 -                <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post">
 -                    <div class="row">
 -                        <div class="col-sm-6">
 -                            {!! csrf_field() !!}
 -                            <input type="hidden" name="_method" value="put">
 -                            @include('users.forms.' . $authMethod, ['model' => $user])
  
 -                        </div>
 -                        <div class="col-sm-6">
 -                            <div class="form-group" id="logo-control">
 -                                <label for="user-avatar">{{ trans('settings.users_avatar') }}</label>
 -                                <p class="small">{{ trans('settings.users_avatar_desc') }}</p>
 +        <div class="py-m">
 +            @include('settings.navbar', ['selected' => 'users'])
 +        </div>
  
 -                                @include('components.image-picker', [
 -                                      'resizeHeight' => '512',
 -                                      'resizeWidth' => '512',
 -                                      'showRemove' => false,
 -                                      'defaultImage' => baseUrl('/user_avatar.png'),
 -                                      'currentImage' => $user->getAvatar(80),
 -                                      'currentId' => $user->image_id,
 -                                      'name' => 'image_id',
 -                                      'imageClass' => 'avatar large'
 -                                  ])
 -                            </div>
 -                            <div class="form-group">
 -                                <label for="user-language">{{ trans('settings.users_preferred_language') }}</label>
 -                                <select name="setting[language]" id="user-language">
 +        <div class="card content-wrap">
 +            <h1 class="list-heading">{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
 +            <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post">
 +                {!! csrf_field() !!}
 +                <input type="hidden" name="_method" value="PUT">
  
 -                                    @foreach(trans('settings.language_select') as $lang => $label)
 -                                        <option @if(setting()->getUser($user, 'language', config('app.default_locale')) === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
 -                                    @endforeach
 -                                </select>
 -                            </div>
 +                <div class="setting-list">
 +                    @include('users.form', ['model' => $user, 'authMethod' => $authMethod])
 +
 +                    <div class="grid half large-gap">
 +                        <div>
 +                            <label for="user-avatar" class="setting-list-label">{{ trans('settings.users_avatar') }}</label>
 +                            <p class="small">{{ trans('settings.users_avatar_desc') }}</p>
 +                        </div>
 +                        <div>
 +                            @include('components.image-picker', [
 +                                'resizeHeight' => '512',
 +                                'resizeWidth' => '512',
 +                                'showRemove' => false,
 +                                'defaultImage' => baseUrl('/user_avatar.png'),
 +                                'currentImage' => $user->getAvatar(80),
 +                                'currentId' => $user->image_id,
 +                                'name' => 'image_id',
 +                                'imageClass' => 'avatar large'
 +                            ])
                          </div>
                      </div>
 -                    <div class="form-group text-right">
 -                        <a href="{{  baseUrl($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
 -                        @if($authMethod !== 'system')
 -                            <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button">{{ trans('settings.users_delete') }}</a>
 -                        @endif
 -                        <button class="button pos" type="submit">{{ trans('common.save') }}</button>
 +
 +                    <div class="grid half large-gap v-center">
 +                        <div>
 +                            <label for="user-language" class="setting-list-label">{{ trans('settings.users_preferred_language') }}</label>
 +                            <p class="small">
 +                                {{ trans('settings.users_preferred_language_desc') }}
 +                            </p>
 +                        </div>
 +                        <div>
 +                            <select name="setting[language]" id="user-language">
 +                                @foreach(trans('settings.language_select') as $lang => $label)
-                                     <option @if(setting()->getUser($user, 'language') === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
++                                    <option @if(setting()->getUser($user, 'language', config('app.default_locale')) === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
 +                                @endforeach
 +                            </select>
 +                        </div>
                      </div>
 -                </form>
 -            </div>
 +
 +                </div>
 +
 +                <div class="text-right">
 +                    <a href="{{  baseUrl($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
 +                    @if($authMethod !== 'system')
 +                        <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="button outline">{{ trans('settings.users_delete') }}</a>
 +                    @endif
 +                    <button class="button primary" type="submit">{{ trans('common.save') }}</button>
 +                </div>
 +            </form>
          </div>
  
          @if($currentUser->id === $user->id && count($activeSocialDrivers) > 0)
 -            <div class="card">
 -                <h3>@icon('login')  {{ trans('settings.users_social_accounts') }}</h3>
 -                <div class="body">
 -                    <p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
 -                    <div class="container">
 -                        <div class="row">
 -                            @foreach($activeSocialDrivers as $driver => $enabled)
 -                                <div class="col-sm-4 col-xs-6 text-center">
 -                                    <div>@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
 -                                    <div>
 -                                        @if($user->hasSocialAccount($driver))
 -                                            <a href="{{ baseUrl("/login/service/{$driver}/detach") }}" class="button neg">{{ trans('settings.users_social_disconnect') }}</a>
 -                                        @else
 -                                            <a href="{{ baseUrl("/login/service/{$driver}") }}" class="button pos">{{ trans('settings.users_social_connect') }}</a>
 -                                        @endif
 -                                    </div>
 -                                    <div>&nbsp;</div>
 +            <div class="card content-wrap auto-height">
 +                <h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
 +                <p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
 +                <div class="container">
 +                    <div class="grid third">
 +                        @foreach($activeSocialDrivers as $driver => $enabled)
 +                            <div class="text-center mb-m">
 +                                <div>@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
 +                                <div>
 +                                    @if($user->hasSocialAccount($driver))
 +                                        <a href="{{ baseUrl("/login/service/{$driver}/detach") }}" class="button small outline">{{ trans('settings.users_social_disconnect') }}</a>
 +                                    @else
 +                                        <a href="{{ baseUrl("/login/service/{$driver}") }}" class="button small outline">{{ trans('settings.users_social_connect') }}</a>
 +                                    @endif
                                  </div>
 -                            @endforeach
 -                        </div>
 +                            </div>
 +                        @endforeach
                      </div>
                  </div>
              </div>
          @endif
 -
 -
      </div>
  
 -    <p class="margin-top large"><br></p>
      @include('components.image-manager', ['imageType' => 'user'])
--@stop
++@stop