From 30458405ce55ca84493e197449ad48f64a8c4cd6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 23 Oct 2016 17:55:48 +0100 Subject: [PATCH] Page Attachments - Improved UI, Now initially complete Closes #62 --- app/File.php | 2 +- app/Http/Controllers/Controller.php | 22 +++- app/Http/Controllers/FileController.php | 8 +- resources/assets/js/controllers.js | 51 ++++++--- resources/assets/js/directives.js | 65 ++++++++++- resources/assets/sass/_components.scss | 14 +++ resources/assets/sass/_pages.scss | 7 +- resources/assets/sass/_tables.scss | 10 ++ resources/views/pages/form-toolbox.blade.php | 108 ++++++++++++------ .../views/pages/sidebar-tree-list.blade.php | 2 +- .../views/partials/custom-styles.blade.php | 2 +- 11 files changed, 222 insertions(+), 69 deletions(-) diff --git a/app/File.php b/app/File.php index 152350c70a9..e9b77d2ea19 100644 --- a/app/File.php +++ b/app/File.php @@ -30,7 +30,7 @@ public function page() */ public function getUrl() { - return '/files/' . $this->id; + return baseUrl('/files/' . $this->id); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index ac430065a70..2b6c88fe0b7 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -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 @@ protected function jsonError($messageText = "", $statusCode = 500) 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()); + } + } diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 88200ae65dd..668e9ec6c04 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -101,8 +101,8 @@ public function update($fileId, Request $request) { $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 @@ public function attachLink(Request $request) { $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'); diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index f098e01306a..99cf6af9d65 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -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]) + } } } diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 82cb128f34a..fa6c2c3be03 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -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); }); } diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 7de42d43c60..2f9051a5258 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -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 diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index 1f79c38c821..c7d3e037731 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -150,7 +150,6 @@ background-color: #FFF; border: 1px solid #DDD; right: $-xl*2; - z-index: 99; width: 48px; overflow: hidden; align-items: stretch; @@ -201,7 +200,7 @@ color: #444; background-color: rgba(0, 0, 0, 0.1); } - div[tab-content] { + div[toolbox-tab-content] { padding-bottom: 45px; display: flex; flex: 1; @@ -209,7 +208,7 @@ min-height: 0px; overflow-y: scroll; } - div[tab-content] .padded { + div[toolbox-tab-content] .padded { flex: 1; padding-top: 0; } @@ -241,7 +240,7 @@ } } -[tab-content] { +[toolbox-tab-content] { display: none; } diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss index 1fc8e11c226..37c61159db0 100644 --- a/resources/assets/sass/_tables.scss +++ b/resources/assets/sass/_tables.scss @@ -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 diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index e6b761c28b1..78e485eabff 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -3,13 +3,13 @@
- + @if(userCan('file-create-all')) - + @endif
-
+

Page Tags

Add some tags to better categorise your content.
You can assign a value to a tag for more in-depth organisation.

@@ -38,55 +38,93 @@
@if(userCan('file-create-all')) -
-

Attached Files

+
+

Attachments

-

Upload some files to display on your page. This are visible in the page sidebar.

- +

Upload some files or attach some link to display on your page. This are visible in the page sidebar.

-
+
+ +
+ + + + + + + + + + +
+ +
+ Click delete again to confirm you want to delete this attachment. +
+ Cancel +
+
+

+ No files have been uploaded. +

+
+
+ +
+
+

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.

+
+ + +

+
+
+ + +

+
+ -
- - -
-
- - +
- - - - - - - - - - - -
-
+
Edit File
+
+

-
- -
-
- - + +
+ +
+ +
+
+
+
+ + +

+
+
- +
diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index f6b834f0755..8e7db85ac21 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -5,7 +5,7 @@
Attachments
@foreach($page->files as $file) @endforeach @endif diff --git a/resources/views/partials/custom-styles.blade.php b/resources/views/partials/custom-styles.blade.php index bf7dde1d4a3..885cc2729c9 100644 --- a/resources/views/partials/custom-styles.blade.php +++ b/resources/views/partials/custom-styles.blade.php @@ -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') }}; } \ No newline at end of file