*/
public function getUrl()
{
- return '/files/' . $this->id;
+ return baseUrl('/files/' . $this->id);
}
}
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
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());
+ }
+
}
{
$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');
{
$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');
$scope.files = [];
$scope.editFile = false;
$scope.file = getCleanFile();
+ $scope.errors = {
+ link: {},
+ edit: {}
+ };
function getCleanFile() {
return {
currentOrder = newOrder;
$http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => {
events.emit('success', resp.data.message);
- }, checkError);
+ }, checkError('sort'));
}
/**
$http.get(url).then(resp => {
$scope.files = resp.data;
currentOrder = resp.data.map(file => {return file.id}).join(':');
- }, checkError);
+ }, checkError('get'));
}
getFiles();
*/
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
- $scope.files.unshift(data);
+ $scope.files.push(data);
});
events.emit('success', 'File uploaded');
};
$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 = '';
}
});
* @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'));
};
/**
$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'));
};
/**
* @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 : '';
};
/**
$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.
* 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])
+ }
}
}
};
});
+ /**
+ * 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
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
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);
});
}
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
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;
}
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
<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>
<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
.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