mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-21 15:36:58 +00:00
parent
91220239e5
commit
30458405ce
11 changed files with 222 additions and 69 deletions
app
resources
|
@ -30,7 +30,7 @@ class File extends Ownable
|
|||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return '/files/' . $this->id;
|
||||
return baseUrl('/files/' . $this->id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
@ -38,55 +38,93 @@
|
|||
</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>
|
||||
<div class="form-group">
|
||||
<label for="attachment-via-link">Link to file</label>
|
||||
<input type="text" placeholder="File url" ng-model="file.link">
|
||||
</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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue