]> BookStack Code Mirror - bookstack/commitdiff
Page Attachments - Improved UI, Now initially complete 205/head
authorDan Brown <redacted>
Sun, 23 Oct 2016 16:55:48 +0000 (17:55 +0100)
committerDan Brown <redacted>
Sun, 23 Oct 2016 16:55:48 +0000 (17:55 +0100)
Closes #62

app/File.php
app/Http/Controllers/Controller.php
app/Http/Controllers/FileController.php
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/sass/_components.scss
resources/assets/sass/_pages.scss
resources/assets/sass/_tables.scss
resources/views/pages/form-toolbox.blade.php
resources/views/pages/sidebar-tree-list.blade.php
resources/views/partials/custom-styles.blade.php

index 152350c70a98076911c741c36eed397cdc2f8e15..e9b77d2ea192b4ddaafd7e3814b98d7ac6fa1b3c 100644 (file)
@@ -30,7 +30,7 @@ class File extends Ownable
      */
     public function getUrl()
     {
-        return '/files/' . $this->id;
+        return baseUrl('/files/' . $this->id);
     }
 
 }
index ac430065a70caae150bf880044371193b55468d0..2b6c88fe0b73748ab5cbdf61a64138dcdc4d5c49 100644 (file)
@@ -3,13 +3,11 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Ownable;
-use HttpRequestException;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Http\Exception\HttpResponseException;
+use Illuminate\Http\Request;
 use Illuminate\Routing\Controller as BaseController;
 use Illuminate\Foundation\Validation\ValidatesRequests;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Session;
 use BookStack\User;
 
 abstract class Controller extends BaseController
@@ -130,4 +128,22 @@ abstract class Controller extends BaseController
         return response()->json(['message' => $messageText], $statusCode);
     }
 
+    /**
+     * Create the response for when a request fails validation.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  array  $errors
+     * @return \Symfony\Component\HttpFoundation\Response
+     */
+    protected function buildFailedValidationResponse(Request $request, array $errors)
+    {
+        if ($request->expectsJson()) {
+            return response()->json(['validation' => $errors], 422);
+        }
+
+        return redirect()->to($this->getRedirectUrl())
+            ->withInput($request->input())
+            ->withErrors($errors, $this->errorBag());
+    }
+
 }
index 88200ae65ddbe7951da8f8958e98a4a22b72ef13..668e9ec6c04c6a19eb96c8ac8c179aac6340f191 100644 (file)
@@ -101,8 +101,8 @@ class FileController extends Controller
     {
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id',
-            'name' => 'string|max:255',
-            'link' =>  'url'
+            'name' => 'required|string|min:1|max:255',
+            'link' =>  'url|min:1|max:255'
         ]);
 
         $pageId = $request->get('uploaded_to');
@@ -129,8 +129,8 @@ class FileController extends Controller
     {
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id',
-            'name' => 'string|max:255',
-            'link' =>  'url|max:255'
+            'name' => 'required|string|min:1|max:255',
+            'link' =>  'required|url|min:1|max:255'
         ]);
 
         $pageId = $request->get('uploaded_to');
index f098e01306a9950ee2dd194cc3f2ef2677d451bd..99cf6af9d6512f945f30c93ad1381cebec6aea9d 100644 (file)
@@ -538,6 +538,10 @@ module.exports = function (ngApp, events) {
             $scope.files = [];
             $scope.editFile = false;
             $scope.file = getCleanFile();
+            $scope.errors = {
+                link: {},
+                edit: {}
+            };
 
             function getCleanFile() {
                 return {
@@ -567,7 +571,7 @@ module.exports = function (ngApp, events) {
                 currentOrder = newOrder;
                 $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => {
                     events.emit('success', resp.data.message);
-                }, checkError);
+                }, checkError('sort'));
             }
 
             /**
@@ -587,7 +591,7 @@ module.exports = function (ngApp, events) {
                 $http.get(url).then(resp => {
                     $scope.files = resp.data;
                     currentOrder = resp.data.map(file => {return file.id}).join(':');
-                }, checkError);
+                }, checkError('get'));
             }
             getFiles();
 
@@ -599,7 +603,7 @@ module.exports = function (ngApp, events) {
              */
             $scope.uploadSuccess = function (file, data) {
                 $scope.$apply(() => {
-                    $scope.files.unshift(data);
+                    $scope.files.push(data);
                 });
                 events.emit('success', 'File uploaded');
             };
@@ -612,10 +616,10 @@ module.exports = function (ngApp, events) {
             $scope.uploadSuccessUpdate = function (file, data) {
                 $scope.$apply(() => {
                     let search = filesIndexOf(data);
-                    if (search !== -1) $scope.files[search] = file;
+                    if (search !== -1) $scope.files[search] = data;
 
                     if ($scope.editFile) {
-                        $scope.editFile = data;
+                        $scope.editFile = angular.copy(data);
                         data.link = '';
                     }
                 });
@@ -627,10 +631,14 @@ module.exports = function (ngApp, events) {
              * @param file
              */
             $scope.deleteFile = function(file) {
+                if (!file.deleting) {
+                    file.deleting = true;
+                    return;
+                }
                   $http.delete(`/files/${file.id}`).then(resp => {
                       events.emit('success', resp.data.message);
                       $scope.files.splice($scope.files.indexOf(file), 1);
-                  }, checkError);
+                  }, checkError('delete'));
             };
 
             /**
@@ -641,10 +649,10 @@ module.exports = function (ngApp, events) {
             $scope.attachLinkSubmit = function(file) {
                 file.uploaded_to = pageId;
                 $http.post('/files/link', file).then(resp => {
-                    $scope.files.unshift(resp.data);
+                    $scope.files.push(resp.data);
                     events.emit('success', 'Link attached');
                     $scope.file = getCleanFile();
-                }, checkError);
+                }, checkError('link'));
             };
 
             /**
@@ -652,8 +660,9 @@ module.exports = function (ngApp, events) {
              * @param fileId
              */
             $scope.startEdit = function(file) {
+                console.log(file);
                 $scope.editFile = angular.copy(file);
-                if (!file.external) $scope.editFile.link = '';
+                $scope.editFile.link = (file.external) ? file.path : '';
             };
 
             /**
@@ -670,16 +679,23 @@ module.exports = function (ngApp, events) {
             $scope.updateFile = function(file) {
                 $http.put(`/files/${file.id}`, file).then(resp => {
                     let search = filesIndexOf(resp.data);
-                    if (search !== -1) $scope.files[search] = file;
+                    if (search !== -1) $scope.files[search] = resp.data;
 
                     if ($scope.editFile && !file.external) {
                         $scope.editFile.link = '';
                     }
                     $scope.editFile = false;
                     events.emit('success', 'Attachment details updated');
-                });
+                }, checkError('edit'));
             };
 
+            /**
+             * Get the url of a file.
+             */
+            $scope.getFileUrl = function(file) {
+                return window.baseUrl('/files/' + file.id);
+            }
+
             /**
              * Search the local files via another file object.
              * Used to search via object copies.
@@ -697,9 +713,16 @@ module.exports = function (ngApp, events) {
              * Check for an error response in a ajax request.
              * @param response
              */
-            function checkError(response) {
-                if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
-                    events.emit('error', response.data.error);
+            function checkError(errorGroupName) {
+                $scope.errors[errorGroupName] = {};
+                return function(response) {
+                    if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
+                        events.emit('error', response.data.error);
+                    }
+                    if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
+                        $scope.errors[errorGroupName] = response.data.validation;
+                        console.log($scope.errors[errorGroupName])
+                    }
                 }
             }
 
index 82cb128f34a3a999476e667184cdacbb98cd3f2f..fa6c2c3be03f26a540bf4081cc312d81b153ee13 100644 (file)
@@ -33,6 +33,59 @@ module.exports = function (ngApp, events) {
         };
     });
 
+    /**
+     * Common tab controls using simple jQuery functions.
+     */
+    ngApp.directive('tabContainer', function() {
+        return {
+            restrict: 'A',
+            link: function (scope, element, attrs) {
+                const $content = element.find('[tab-content]');
+                const $buttons = element.find('[tab-button]');
+
+                if (attrs.tabContainer) {
+                    let initial = attrs.tabContainer;
+                    $buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
+                    $content.hide().filter(`[tab-content="${initial}"]`).show();
+                } else {
+                    $content.hide().first().show();
+                    $buttons.first().addClass('selected');
+                }
+
+                $buttons.click(function() {
+                    let clickedTab = $(this);
+                    $buttons.removeClass('selected');
+                    $content.hide();
+                    let name = clickedTab.addClass('selected').attr('tab-button');
+                    $content.filter(`[tab-content="${name}"]`).show();
+                });
+            }
+        };
+    });
+
+    /**
+     * Sub form component to allow inner-form sections to act like thier own forms.
+     */
+    ngApp.directive('subForm', function() {
+        return {
+            restrict: 'A',
+            link: function (scope, element, attrs) {
+                element.on('keypress', e => {
+                    if (e.keyCode === 13) {
+                        submitEvent(e);
+                    }
+                });
+
+                element.find('button[type="submit"]').click(submitEvent);
+                
+                function submitEvent(e) {
+                    e.preventDefault()
+                    if (attrs.subForm) scope.$eval(attrs.subForm);
+                }
+            }
+        };
+    });
+
 
     /**
      * Image Picker
@@ -489,8 +542,8 @@ module.exports = function (ngApp, events) {
             link: function (scope, elem, attrs) {
 
                 // Get common elements
-                const $buttons = elem.find('[tab-button]');
-                const $content = elem.find('[tab-content]');
+                const $buttons = elem.find('[toolbox-tab-button]');
+                const $content = elem.find('[toolbox-tab-content]');
                 const $toggle = elem.find('[toolbox-toggle]');
 
                 // Handle toolbox toggle click
@@ -502,17 +555,17 @@ module.exports = function (ngApp, events) {
                 function setActive(tabName, openToolbox) {
                     $buttons.removeClass('active');
                     $content.hide();
-                    $buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
-                    $content.filter(`[tab-content="${tabName}"]`).show();
+                    $buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
+                    $content.filter(`[toolbox-tab-content="${tabName}"]`).show();
                     if (openToolbox) elem.addClass('open');
                 }
 
                 // Set the first tab content active on load
-                setActive($content.first().attr('tab-content'), false);
+                setActive($content.first().attr('toolbox-tab-content'), false);
 
                 // Handle tab button click
                 $buttons.click(function (e) {
-                    let name = $(this).attr('tab-button');
+                    let name = $(this).attr('toolbox-tab-button');
                     setActive(name, true);
                 });
             }
index 7de42d43c6074bacb3d06f1a874dbc11d9ae61cf..2f9051a5258e94d9a821fd71c73aac30569c1501 100644 (file)
@@ -452,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   border-right: 6px solid transparent;
   border-bottom: 6px solid $negative;
 }
+
+
+[tab-container] .nav-tabs {
+  text-align: left;
+  border-bottom: 1px solid #DDD;
+  margin-bottom: $-m;
+  .tab-item {
+    padding: $-s;
+    color: #666;
+    &.selected {
+      border-bottom-width: 3px;
+    }
+  }
+}
\ No newline at end of file
index 1f79c38c821325a264e321997813c0586b1213ad..c7d3e037731385229277f4871ace9432c43336bf 100755 (executable)
   background-color: #FFF;
   border: 1px solid #DDD;
   right: $-xl*2;
-  z-index: 99;
   width: 48px;
   overflow: hidden;
   align-items: stretch;
     color: #444;
     background-color: rgba(0, 0, 0, 0.1);
   }
-  div[tab-content] {
+  div[toolbox-tab-content] {
     padding-bottom: 45px;
     display: flex;
     flex: 1;
     min-height: 0px;
     overflow-y: scroll;
   }
-  div[tab-content] .padded {
+  div[toolbox-tab-content] .padded {
     flex: 1;
     padding-top: 0;
   }
   }
 }
 
-[tab-content] {
+[toolbox-tab-content] {
   display: none;
 }
 
index 1fc8e11c2266a14ec2edfe70f2fa19fb87832ede..37c61159db077488d7b7d9ed3988bf81a20aa7f2 100644 (file)
@@ -51,4 +51,14 @@ table.list-table {
     vertical-align: middle;
     padding: $-xs;
   }
+}
+
+table.file-table {
+  @extend .no-style;
+  td {
+    padding: $-xs;
+  }
+  .ui-sortable-helper {
+    display: table;
+  }
 }
\ No newline at end of file
index e6b761c28b16043a0a048d772d23b5abe27c56d1..78e485eabffdf54467733ed7271f5b5a09f79967 100644 (file)
@@ -3,13 +3,13 @@
 
     <div class="tabs primary-background-light">
         <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
-        <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
+        <span toolbox-tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
         @if(userCan('file-create-all'))
-            <span tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
+            <span toolbox-tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
         @endif
     </div>
 
-    <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
+    <div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
         <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>
     </div>
 
     @if(userCan('file-create-all'))
-        <div tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
-            <h4>Attached Files</h4>
+        <div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
+            <h4>Attachments</h4>
             <div class="padded files">
 
                 <div id="file-list" ng-show="!editFile">
-                    <p class="muted small">Upload some files to display on your page. This are visible in the page sidebar.</p>
-                    <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
+                    <p class="muted small">Upload some files or attach some link to display on your page. This are visible in the page sidebar.</p>
 
-                    <hr class="even">
+                    <div tab-container>
+                        <div class="nav-tabs">
+                            <div tab-button="list" class="tab-item">File List</div>
+                            <div tab-button="file" class="tab-item">Upload File</div>
+                            <div tab-button="link" class="tab-item">Attach Link</div>
+                        </div>
+                        <div tab-content="list">
+                            <table class="file-table" style="width: 100%;">
+                                <tbody ui-sortable="sortOptions" ng-model="files" >
+                                <tr ng-repeat="file in files track by $index">
+                                    <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
+                                    <td>
+                                        <a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a>
+                                        <div ng-if="file.deleting">
+                                            <span class="neg small">Click delete again to confirm you want to delete this attachment.</span>
+                                            <br>
+                                            <span class="text-primary small" ng-click="file.deleting=false;">Cancel</span>
+                                        </div>
+                                    </td>
+                                    <td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
+                                    <td width="5"></td>
+                                    <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
+                                </tr>
+                                </tbody>
+                            </table>
+                            <p class="small muted" ng-if="files.length == 0">
+                                No files have been uploaded.
+                            </p>
+                        </div>
+                        <div tab-content="file">
+                            <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
+                        </div>
+                        <div tab-content="link" sub-form="attachLinkSubmit(file)">
+                            <p class="muted small">You can attach a link if you'd prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.</p>
+                            <div class="form-group">
+                                <label for="attachment-via-link">Link Name</label>
+                                <input type="text" placeholder="Link name" ng-model="file.name">
+                                <p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
+                            </div>
+                            <div class="form-group">
+                                <label for="attachment-via-link">Link to file</label>
+                                <input type="text" placeholder="Url of site or file" ng-model="file.link">
+                                <p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
+                            </div>
+                            <button type="submit" class="button pos">Attach</button>
 
-                    <div class="form-group">
-                        <label for="attachment-via-link">File Name</label>
-                        <input type="text" placeholder="File name" ng-model="file.name">
-                    </div>
-                    <div class="form-group">
-                        <label for="attachment-via-link">Link to file</label>
-                        <input type="text" placeholder="File url" ng-model="file.link">
+                        </div>
                     </div>
-                    <button type="button" ng-click="attachLinkSubmit(file)" class="button pos">Attach</button>
-
 
-                    <table class="no-style" tag-autosuggestions style="width: 100%;">
-                        <tbody ui-sortable="sortOptions" ng-model="files" >
-                        <tr ng-repeat="file in files track by $index">
-                            <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
-                            <td ng-bind="file.name"></td>
-                            <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
-                            <td width="10" ng-click="startEdit(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
-                        </tr>
-                        </tbody>
-                    </table>
                 </div>
 
-                <div id="file-edit" ng-if="editFile">
+                <div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)">
                     <h5>Edit File</h5>
+
                     <div class="form-group">
                         <label for="attachment-name-edit">File Name</label>
                         <input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
+                        <p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p>
                     </div>
-                    <hr class="even">
-                    <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
-                    <hr class="even">
-                    <div class="form-group">
-                        <label for="attachment-link-edit">Link to file</label>
-                        <input type="text" id="attachment-link-edit" placeholder="File url" ng-model="editFile.link">
+
+                    <div tab-container="@{{ editFile.external ? 'link' : 'file' }}">
+                        <div class="nav-tabs">
+                            <div tab-button="file" class="tab-item">Upload File</div>
+                            <div tab-button="link" class="tab-item">Set Link</div>
+                        </div>
+                        <div tab-content="file">
+                            <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
+                            <br>
+                        </div>
+                        <div tab-content="link">
+                            <div class="form-group">
+                                <label for="attachment-link-edit">Link to file</label>
+                                <input type="text" id="attachment-link-edit" placeholder="Attachment link" ng-model="editFile.link">
+                                <p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
+                            </div>
+                        </div>
                     </div>
 
                     <button type="button" class="button" ng-click="cancelEdit()">Back</button>
-                    <button type="button" class="button pos" ng-click="updateFile(editFile)">Save</button>
+                    <button type="submit" class="button pos">Save</button>
                 </div>
 
             </div>
index f6b834f07553042e0012c7f9327ece56c93f3678..8e7db85ac21c7f0523677c2b4dc0b37cea9cfce8 100644 (file)
@@ -5,7 +5,7 @@
         <h6 class="text-muted">Attachments</h6>
         @foreach($page->files as $file)
             <div class="attachment">
-                <a href="{{ $file->getUrl() }}" @if($file->external) target="_blank" @endif><i class="zmdi zmdi-file"></i> {{ $file->name }}</a>
+                <a href="{{ $file->getUrl() }}" @if($file->external) target="_blank" @endif><i class="zmdi zmdi-{{ $file->external ? 'open-in-new' : 'file' }}"></i> {{ $file->name }}</a>
             </div>
         @endforeach
     @endif
index bf7dde1d4a34de2344ee1a32d49ba6b5101e166a..885cc2729c9c1a2386fe65e69a89c37a14be4ee8 100644 (file)
@@ -14,7 +14,7 @@
     .nav-tabs a.selected, .nav-tabs .tab-item.selected {
         border-bottom-color: {{ setting('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 {
+    .text-primary, p.primary, p .primary, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
         color: {{ setting('app-color') }};
     }
 </style>
\ No newline at end of file