]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'v0.11'
authorDan Brown <redacted>
Sun, 14 Aug 2016 12:09:44 +0000 (13:09 +0100)
committerDan Brown <redacted>
Sun, 14 Aug 2016 12:09:44 +0000 (13:09 +0100)
16 files changed:
.github/ISSUE_TEMPLATE.md [new file with mode: 0644]
.travis.yml
app/PageRevision.php
app/Repos/PageRepo.php
app/Services/ImageService.php
database/migrations/2016_07_07_181521_add_summary_to_page_revisions.php [new file with mode: 0644]
resources/assets/js/directives.js
resources/assets/js/global.js
resources/assets/js/pages/page-form.js
resources/assets/sass/_grid.scss
resources/assets/sass/_header.scss
resources/assets/sass/_lists.scss
resources/views/pages/form.blade.php
resources/views/pages/restrictions.blade.php
resources/views/pages/revisions.blade.php
tests/Entity/EntityTest.php

diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644 (file)
index 0000000..4f9f4c4
--- /dev/null
@@ -0,0 +1,11 @@
+### For Feature Requests
+Desired Feature:
+
+### For Bug Reports
+PHP Version:
+
+MySQL Version:
+
+Expected Behavior:
+
+Actual Behavior:
index 83e9e10f5edff887590f9f3d7cbe8217252a4fc8..bea8d179597f920b1fa59ebf9249d5faf1e15305 100644 (file)
@@ -25,6 +25,7 @@ before_script:
   - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
   - phpenv config-rm xdebug.ini
   - composer self-update
+  - composer dump-autoload --no-interaction
   - composer install --prefer-dist --no-interaction
   - npm install
   - ./node_modules/.bin/gulp
index dae74cd0f9683af8775c1422173275baa9453dc3..1ffd63dbd584700376c983e39a4f4c1c8ce36800 100644 (file)
@@ -3,7 +3,7 @@
 
 class PageRevision extends Model
 {
-    protected $fillable = ['name', 'html', 'text', 'markdown'];
+    protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
 
     /**
      * Get the user that created the page revision
index 3698e5efb925994a547b0252d3850369b639ca18..235246f823ef9696e1e97e934da79605dffe31b2 100644 (file)
@@ -157,6 +157,8 @@ class PageRepo extends EntityRepo
         $draftPage->draft = false;
 
         $draftPage->save();
+        $this->saveRevision($draftPage, 'Initial Publish');
+        
         return $draftPage;
     }
 
@@ -308,10 +310,9 @@ class PageRepo extends EntityRepo
      */
     public function updatePage(Page $page, $book_id, $input)
     {
-        // Save a revision before updating
-        if ($page->html !== $input['html'] || $page->name !== $input['name']) {
-            $this->saveRevision($page);
-        }
+        // Hold the old details to compare later
+        $oldHtml = $page->html;
+        $oldName = $page->name;
 
         // Prevent slug being updated if no name change
         if ($page->name !== $input['name']) {
@@ -335,6 +336,11 @@ class PageRepo extends EntityRepo
         // Remove all update drafts for this user & page.
         $this->userUpdateDraftsQuery($page, $userId)->delete();
 
+        // Save a revision after updating
+        if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
+            $this->saveRevision($page, $input['summary']);
+        }
+
         return $page;
     }
 
@@ -360,9 +366,10 @@ class PageRepo extends EntityRepo
     /**
      * Saves a page revision into the system.
      * @param Page $page
+     * @param null|string $summary
      * @return $this
      */
-    public function saveRevision(Page $page)
+    public function saveRevision(Page $page, $summary = null)
     {
         $revision = $this->pageRevision->fill($page->toArray());
         if (setting('app-editor') !== 'markdown') $revision->markdown = '';
@@ -372,6 +379,7 @@ class PageRepo extends EntityRepo
         $revision->created_by = auth()->user()->id;
         $revision->created_at = $page->updated_at;
         $revision->type = 'version';
+        $revision->summary = $summary;
         $revision->save();
         // Clear old revisions
         if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
index d9bd61e9fb4ef79cf0174f610a1122d751e6adf1..4401cb230a8b4105ba5458be1e4e88f596803ccb 100644 (file)
@@ -95,6 +95,7 @@ class ImageService
 
         try {
             $storage->put($fullPath, $imageData);
+            $storage->setVisibility($fullPath, 'public');
         } catch (Exception $e) {
             throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
         }
@@ -167,6 +168,7 @@ class ImageService
 
         $thumbData = (string)$thumb->encode();
         $storage->put($thumbFilePath, $thumbData);
+        $storage->setVisibility($thumbFilePath, 'public');
         $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
 
         return $this->getPublicUrl($thumbFilePath);
@@ -257,9 +259,15 @@ class ImageService
             $storageUrl = config('filesystems.url');
 
             // Get the standard public s3 url if s3 is set as storage type
+            // Uses the nice, short URL if bucket name has no periods in otherwise the longer
+            // region-based url will be used to prevent http issues.
             if ($storageUrl == false && config('filesystems.default') === 's3') {
                 $storageDetails = config('filesystems.disks.s3');
-                $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
+                if (strpos($storageDetails['bucket'], '.') === false) {
+                    $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
+                } else {
+                    $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
+                }
             }
 
             $this->storageUrl = $storageUrl;
@@ -269,4 +277,4 @@ class ImageService
     }
 
 
-}
\ No newline at end of file
+}
diff --git a/database/migrations/2016_07_07_181521_add_summary_to_page_revisions.php b/database/migrations/2016_07_07_181521_add_summary_to_page_revisions.php
new file mode 100644 (file)
index 0000000..c618877
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddSummaryToPageRevisions extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('page_revisions', function ($table) {
+            $table->string('summary')->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('page_revisions', function ($table) {
+            $table->dropColumn('summary');
+        });
+    }
+}
index 897707af56df79481ff61df42e873fed75324598..fc7b88259eccb92b17e4a2305abe75d96bd66027 100644 (file)
@@ -158,9 +158,22 @@ module.exports = function (ngApp, events) {
         return {
             restrict: 'A',
             link: function (scope, element, attrs) {
-                var menu = element.find('ul');
+                const menu = element.find('ul');
                 element.find('[dropdown-toggle]').on('click', function () {
                     menu.show().addClass('anim menuIn');
+                    let inputs = menu.find('input');
+                    let hasInput = inputs.length > 0;
+                    if (hasInput) {
+                        inputs.first().focus();
+                        element.on('keypress', 'input', event => {
+                            if (event.keyCode === 13) {
+                                event.preventDefault();
+                                menu.hide();
+                                menu.removeClass('anim menuIn');
+                                return false;
+                            }
+                        });
+                    }
                     element.mouseleave(function () {
                         menu.hide();
                         menu.removeClass('anim menuIn');
index 1c300ad26b209a671146bc657e2176308823c2bb..3a107afa8d29584fdf397fadee2fefac1d95135c 100644 (file)
@@ -18,9 +18,12 @@ window.baseUrl = function(path) {
 var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
 
 // Global Event System
-var Events = {
-    listeners: {},
-    emit: function (eventName, eventData) {
+class Events {
+    constructor() {
+        this.listeners = {};
+    }
+
+    emit(eventName, eventData) {
         if (typeof this.listeners[eventName] === 'undefined') return this;
         var eventsToStart = this.listeners[eventName];
         for (let i = 0; i < eventsToStart.length; i++) {
@@ -28,14 +31,15 @@ var Events = {
             event(eventData);
         }
         return this;
-    },
-    listen: function (eventName, callback) {
+    }
+
+    listen(eventName, callback) {
         if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
         this.listeners[eventName].push(callback);
         return this;
     }
 };
-window.Events = Events;
+window.Events = new Events();
 
 
 var services = require('./services')(ngApp, Events);
@@ -47,14 +51,15 @@ var controllers = require('./controllers')(ngApp, Events);
 // Smooth scrolling
 jQuery.fn.smoothScrollTo = function () {
     if (this.length === 0) return;
-    $('body').animate({
+    let scrollElem = document.documentElement.scrollTop === 0 ?  document.body : document.documentElement;
+    $(scrollElem).animate({
         scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
     }, 800); // Adjust to change animations speed (ms)
     return this;
 };
 
 // Making contains text expression not worry about casing
-$.expr[":"].contains = $.expr.createPseudo(function (arg) {
+jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
     return function (elem) {
         return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
     };
@@ -104,13 +109,14 @@ $(function () {
     var scrollTop = document.getElementById('back-to-top');
     var scrollTopBreakpoint = 1200;
     window.addEventListener('scroll', function() {
-        if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
+        let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
+        if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
             scrollTop.style.display = 'block';
             scrollTopShowing = true;
             setTimeout(() => {
                 scrollTop.style.opacity = 0.4;
             }, 1);
-        } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
+        } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
             scrollTop.style.opacity = 0;
             scrollTopShowing = false;
             setTimeout(() => {
index f8b314e9cb914e49ddbc214054af04988d086237..86678a1bada517ce3156569b1bf57f5a5ba223f2 100644 (file)
@@ -1,3 +1,60 @@
+"use strict";
+
+function editorPaste(e) {
+    if (!e.clipboardData) return
+    var items = e.clipboardData.items;
+    if (!items) return;
+    for (var i = 0; i < items.length; i++) {
+        if (items[i].type.indexOf("image") !== -1) {
+
+            var file = items[i].getAsFile();
+            var formData = new FormData();
+            var ext = 'png';
+            var xhr = new XMLHttpRequest();
+
+            if (file.name) {
+                var fileNameMatches = file.name.match(/\.(.+)$/);
+                if (fileNameMatches) {
+                    ext = fileNameMatches[1];
+                }
+            }
+
+            var id = "image-" + Math.random().toString(16).slice(2);
+            editor.execCommand('mceInsertContent', false, '<img src="/loading.gif" id="' + id + '">');
+
+            var remoteFilename = "image-" + Date.now() + "." + ext;
+            formData.append('file', file, remoteFilename);
+            formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
+
+            xhr.open('POST', window.baseUrl('/images/gallery/upload'));
+            xhr.onload = function () {
+                if (xhr.status === 200 || xhr.status === 201) {
+                    var result = JSON.parse(xhr.responseText);
+                    editor.dom.setAttrib(id, 'src', result.url);
+                } else {
+                    console.log('An error occured uploading the image');
+                    console.log(xhr.responseText);
+                    editor.dom.remove(id);
+                }
+            };
+            xhr.send(formData);
+        }
+    }
+}
+
+function registerEditorShortcuts(editor) {
+    // Headers
+    for (let i = 1; i < 5; i++) {
+        editor.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
+    }
+
+    // Other block shortcuts
+    editor.addShortcut('ctrl+q', '', ['FormatBlock', false, 'blockquote']);
+    editor.addShortcut('ctrl+d', '', ['FormatBlock', false, 'p']);
+    editor.addShortcut('ctrl+e', '', ['FormatBlock', false, 'pre']);
+    editor.addShortcut('ctrl+s', '', ['FormatBlock', false, 'code']);
+}
+
 var mceOptions = module.exports = {
     selector: '#html-editor',
     content_css: [
@@ -66,6 +123,8 @@ var mceOptions = module.exports = {
             mceOptions.extraSetups[i](editor);
         }
 
+        registerEditorShortcuts(editor);
+
         (function () {
             var wrap;
 
@@ -122,49 +181,6 @@ var mceOptions = module.exports = {
         });
 
         // Paste image-uploads
-        editor.on('paste', function (e) {
-            if (e.clipboardData) {
-                var items = e.clipboardData.items;
-                if (items) {
-                    for (var i = 0; i < items.length; i++) {
-                        if (items[i].type.indexOf("image") !== -1) {
-
-                            var file = items[i].getAsFile();
-                            var formData = new FormData();
-                            var ext = 'png';
-                            var xhr = new XMLHttpRequest();
-
-                            if (file.name) {
-                                var fileNameMatches = file.name.match(/\.(.+)$/);
-                                if (fileNameMatches) {
-                                    ext = fileNameMatches[1];
-                                }
-                            }
-
-                            var id = "image-" + Math.random().toString(16).slice(2);
-                            editor.execCommand('mceInsertContent', false, '<img src="/loading.gif" id="' + id + '">');
-
-                            var remoteFilename = "image-" + Date.now() + "." + ext;
-                            formData.append('file', file, remoteFilename);
-                            formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
-
-                            xhr.open('POST', window.baseUrl('/images/gallery/upload'));
-                            xhr.onload = function () {
-                                if (xhr.status === 200 || xhr.status === 201) {
-                                    var result = JSON.parse(xhr.responseText);
-                                    editor.dom.setAttrib(id, 'src', result.url);
-                                } else {
-                                    console.log('An error occured uploading the image');
-                                    console.log(xhr.responseText);
-                                    editor.dom.remove(id);
-                                }
-                            };
-                            xhr.send(formData);
-                        }
-                    }
-                }
-
-            }
-        });
+        editor.on('paste', editorPaste);
     }
 };
\ No newline at end of file
index 2fe1ad113cb7264ef72e7e9d504b1a0abe5a0916..1a1321e587335c498a3e8c0f8ab3248d01809d4f 100644 (file)
@@ -39,6 +39,9 @@ div[class^="col-"] img {
   &.fluid {
     max-width: 100%;
   }
+  &.medium {
+    max-width: 992px;
+  }
   &.small {
     max-width: 840px;
   }
index e0b1a99cb9121419de399d7aadc2f4365bfe668b..12bd17076a7e85083ae28546b05f54b4f324afe1 100644 (file)
@@ -155,6 +155,7 @@ form.search-box {
       text-decoration: none;
     }
   }
+
 }
 
 .faded span.faded-text {
index 08f00677e9a716401cc15a50d03d1f3e7d64befa..2658c46891bf75ebc99ec9553897be9e93bfa60b 100644 (file)
@@ -375,6 +375,9 @@ ul.pagination {
   .text-muted {
     color: #999;
   }
+  li.padded {
+    padding: $-xs $-m;
+  }
   a {
     display: block;
     padding: $-xs $-m;
@@ -384,10 +387,10 @@ ul.pagination {
       background-color: #EEE;
     }
     i {
-      margin-right: $-m;
+      margin-right: $-s;
       padding-right: 0;
-      display: inline;
-      width: 22px;
+      display: inline-block;
+      width: 16px;
     }
   }
   li.border-bottom {
index 18a9868c7aecd5734acedcb89df35aa8b063cde5..366316b339cf2d472922907dc3a6d93eb553b3c5 100644 (file)
                             <li ng-if="isNewPageDraft">
                                 <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
                             </li>
+                            <li>
+                                <a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</a>
+                            </li>
                         </ul>
                     </div>
                 </div>
                 <div class="col-sm-4 faded">
                     <div class="action-buttons" ng-cloak>
+                        <div dropdown class="dropdown-container">
+                            <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> @{{(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || 'Set Changelog'}}</a>
+                            <ul class="wide">
+                                <li class="padded">
+                                    <p class="text-muted">Enter a brief description of the changes you've made</p>
+                                    <input name="summary" id="summary-input" type="text" placeholder="Enter Changelog" ng-model="changeSummary" />
+                                </li>
+                            </ul>
+                        </div>
 
-                        <button type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button>
-                        <button type="submit" id="save-button" class="text-button  text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
+                        <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
                     </div>
                 </div>
             </div>
index 8eca486c3d13ca8f5bdc50cc70addc7c9f62cbbf..bd88919dfa83c23571e723dca0bcdd1d74ef99c9 100644 (file)
@@ -16,7 +16,7 @@
                             </a>
                         @endif
                         <span class="sep">&raquo;</span>
-                        <a href="{{ $page->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
+                        <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
                     </div>
                 </div>
             </div>
index 03fb23673c09f0fd7c935fe48e1d8ce4813ce4f1..926affffc3824d969518ae57383ed19a53726b00 100644 (file)
@@ -5,45 +5,59 @@
     <div class="faded-small toolbar">
         <div class="container">
             <div class="row">
-                <div class="col-md-6 faded">
+                <div class="col-sm-12 faded">
                     <div class="breadcrumbs">
-                        <a href="{{ $page->getUrl() }}" class="text-primary text-button"><i class="zmdi zmdi-arrow-left"></i>Back to page</a>
+                        <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
+                        @if($page->hasChapter())
+                            <span class="sep">&raquo;</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">&raquo;</span>
+                        <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
                     </div>
                 </div>
-                <div class="col-md-6 faded">
-                </div>
             </div>
         </div>
     </div>
 
 
-    <div class="container small" ng-non-bindable>
+
+    <div class="container" ng-non-bindable>
         <h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1>
 
         @if(count($page->revisions) > 0)
 
             <table class="table">
                 <tr>
-                    <th width="40%">Name</th>
-                    <th colspan="2" width="20%">Created By</th>
-                    <th width="20%">Revision Date</th>
-                    <th width="20%">Actions</th>
+                    <th width="25%">Name</th>
+                    <th colspan="2" width="10%">Created By</th>
+                    <th width="15%">Revision Date</th>
+                    <th width="25%">Changelog</th>
+                    <th width="15%">Actions</th>
                 </tr>
-                @foreach($page->revisions as $revision)
+                @foreach($page->revisions as $index => $revision)
                     <tr>
-                        <td>{{$revision->name}}</td>
+                        <td>{{ $revision->name }}</td>
                         <td style="line-height: 0;">
                             @if($revision->createdBy)
-                                <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{$revision->createdBy->name}}">
+                                <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
                             @endif
                         </td>
-                        <td> @if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif</td>
-                        <td><small>{{$revision->created_at->format('jS F, Y H:i:s')}} <br> ({{$revision->created_at->diffForHumans()}})</small></td>
-                        <td>
-                            <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
-                            <span class="text-muted">&nbsp;|&nbsp;</span>
-                            <a href="{{ $revision->getUrl('/restore') }}">Restore</a>
-                        </td>
+                        <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
+                        <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
+                        <td>{{ $revision->summary }}</td>
+                        @if ($index !== 0)
+                            <td>
+                                <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
+                                <span class="text-muted">&nbsp;|&nbsp;</span>
+                                <a href="{{ $revision->getUrl() }}/restore">Restore</a>
+                            </td>
+                        @else
+                            <td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
+                        @endif
                     </tr>
                 @endforeach
             </table>
index 71d83dd47237efaedd2830af575f8c0f445fe78a..296aa72edc1491ae703e3fab90a92b15db85cf7e 100644 (file)
@@ -218,13 +218,24 @@ class EntityTest extends TestCase
 
     public function test_old_page_slugs_redirect_to_new_pages()
     {
-        $page = \BookStack\Page::all()->first();
+        $page = \BookStack\Page::first();
         $pageUrl = $page->getUrl();
         $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
+        // Need to save twice since revisions are not generated in seeder.
         $this->asAdmin()->visit($pageUrl)
+            ->clickInElement('#content', 'Edit')
+            ->type('super test', '#name')
+            ->press('Save Page');
+
+        $page = \BookStack\Page::first();
+        $pageUrl = $page->getUrl();
+
+        // Second Save
+        $this->visit($pageUrl)
             ->clickInElement('#content', 'Edit')
             ->type('super test page', '#name')
             ->press('Save Page')
+            // Check redirect
             ->seePageIs($newPageUrl)
             ->visit($pageUrl)
             ->seePageIs($newPageUrl);