Finished breakdown of attachment vue into components

This commit is contained in:
Dan Brown 2020-07-04 16:53:02 +01:00
parent 14b6cd1091
commit d41452f39c
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
24 changed files with 371 additions and 321 deletions

View file

@ -8,6 +8,7 @@ use BookStack\Uploads\AttachmentService;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
@ -60,25 +61,17 @@ class AttachmentController extends Controller
/**
* Update an uploaded attachment.
* @throws ValidationException
* @throws NotFoundException
*/
public function uploadUpdate(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$uploadedFile = $request->file('file');
@ -92,57 +85,87 @@ class AttachmentController extends Controller
}
/**
* Update the details of an existing file.
* @throws ValidationException
* @throws NotFoundException
* Get the update form for an attachment.
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function update(Request $request, $attachmentId)
public function getUpdateForm(string $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
return view('attachments.manager-edit-form', [
'attachment' => $attachment,
]);
}
/**
* Update the details of an existing file.
*/
public function update(Request $request, string $attachmentId)
{
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
'attachment_edit_url' => 'string|min:1|max:255'
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
'attachment' => $attachment,
'errors' => new MessageBag($exception->errors()),
]), 422);
}
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
return response()->json($attachment);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
$attachment = $this->attachmentService->updateFile($attachment, [
'name' => $request->get('attachment_edit_name'),
'link' => $request->get('attachment_edit_url'),
]);
return view('attachments.manager-edit-form', [
'attachment' => $attachment,
]);
}
/**
* Attach a link to a page.
* @throws ValidationException
* @throws NotFoundException
*/
public function attachLink(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|string|min:1|max:255'
]);
$pageId = $request->get('attachment_link_uploaded_to');
try {
$this->validate($request, [
'attachment_link_uploaded_to' => 'required|integer|exists:pages,id',
'attachment_link_name' => 'required|string|min:1|max:255',
'attachment_link_url' => 'required|string|min:1|max:255'
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [
'pageId' => $pageId,
'errors' => new MessageBag($exception->errors()),
]), 422);
}
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$attachmentName = $request->get('name');
$link = $request->get('link');
$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
return response()->json($attachment);
return view('attachments.manager-link-form', [
'pageId' => $pageId,
]);
}
/**
@ -152,7 +175,7 @@ class AttachmentController extends Controller
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
return view('pages.attachment-list', [
return view('attachments.manager-list', [
'attachments' => $page->attachments->all(),
]);
}
@ -180,7 +203,7 @@ class AttachmentController extends Controller
* @throws FileNotFoundException
* @throws NotFoundException
*/
public function get(int $attachmentId)
public function get(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
try {
@ -201,11 +224,9 @@ class AttachmentController extends Controller
/**
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
* @throws Exception
*/
public function delete(int $attachmentId)
public function delete(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);

8
package-lock.json generated
View file

@ -3801,14 +3801,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
},
"vuedraggable": {
"version": "2.23.2",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.23.2.tgz",
"integrity": "sha512-PgHCjUpxEAEZJq36ys49HfQmXglattf/7ofOzUrW2/rRdG7tu6fK84ir14t1jYv4kdXewTEa2ieKEAhhEMdwkQ==",
"requires": {
"sortablejs": "^1.10.1"
}
},
"watchpack": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",

View file

@ -28,8 +28,7 @@
"markdown-it": "^11.0.0",
"markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.10.2",
"vue": "^2.6.11",
"vuedraggable": "^2.23.2"
"vue": "^2.6.11"
},
"browser": {
"vue": "vue/dist/vue.common.js"

View file

@ -158,7 +158,7 @@ These are the great open-source projects used to help build BookStack:
* [TinyMCE](https://www.tinymce.com/)
* [CodeMirror](https://codemirror.net)
* [Vue.js](http://vuejs.org/)
* [Sortable](https://github.com/SortableJS/Sortable) & [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)
* [Sortable](https://github.com/SortableJS/Sortable)
* [Google Material Icons](https://material.io/icons/)
* [Dropzone.js](http://www.dropzonejs.com/)
* [clipboard.js](https://clipboardjs.com/)

View file

@ -0,0 +1,58 @@
import {onEnterPress, onSelect} from "../services/dom";
/**
* Ajax Form
* Will handle button clicks or input enter press events and submit
* the data over ajax. Will always expect a partial HTML view to be returned.
* Fires an 'ajax-form-success' event when submitted successfully.
* @extends {Component}
*/
class AjaxForm {
setup() {
this.container = this.$el;
this.url = this.$opts.url;
this.method = this.$opts.method || 'post';
this.successMessage = this.$opts.successMessage;
this.submitButtons = this.$manyRefs.submit || [];
this.setupListeners();
}
setupListeners() {
onEnterPress(this.container, event => {
this.submit();
event.preventDefault();
});
this.submitButtons.forEach(button => onSelect(button, this.submit.bind(this)));
}
async submit() {
const fd = new FormData();
const inputs = this.container.querySelectorAll(`[name]`);
console.log(inputs);
for (const input of inputs) {
fd.append(input.getAttribute('name'), input.value);
}
this.container.style.opacity = '0.7';
this.container.style.pointerEvents = 'none';
try {
const resp = await window.$http[this.method.toLowerCase()](this.url, fd);
this.container.innerHTML = resp.data;
this.$emit('success', {formData: fd});
if (this.successMessage) {
window.$events.emit('success', this.successMessage);
}
} catch (err) {
this.container.innerHTML = err.data;
}
window.components.init(this.container);
this.container.style.opacity = null;
this.container.style.pointerEvents = null;
}
}
export default AjaxForm;

View file

@ -1,14 +1,16 @@
/**
* Attachments
* @extends {Component}
*/
import {showLoading} from "../services/dom";
class Attachments {
setup() {
this.container = this.$el;
this.pageId = this.$opts.pageId;
this.editContainer = this.$refs.editContainer;
this.listContainer = this.$refs.listContainer;
this.mainTabs = this.$refs.mainTabs;
this.list = this.$refs.list;
@ -16,23 +18,30 @@ class Attachments {
}
setupListeners() {
this.container.addEventListener('dropzone-success', event => {
this.mainTabs.components.tabs.show('items');
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data;
window.components.init(this.list);
})
});
const reloadListBound = this.reloadList.bind(this);
this.container.addEventListener('dropzone-success', reloadListBound);
this.container.addEventListener('ajax-form-success', reloadListBound);
this.container.addEventListener('sortable-list-sort', event => {
this.updateOrder(event.detail.ids);
});
this.editContainer.addEventListener('keypress', event => {
if (event.key === 'Enter') {
// TODO - Update editing file
}
})
this.container.addEventListener('event-emit-select-edit', event => {
this.startEdit(event.detail.id);
});
this.container.addEventListener('event-emit-select-edit-back', event => {
this.stopEdit();
});
}
reloadList() {
this.stopEdit();
this.mainTabs.components.tabs.show('items');
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data;
window.components.init(this.list);
});
}
updateOrder(idOrder) {
@ -41,6 +50,21 @@ class Attachments {
});
}
async startEdit(id) {
this.editContainer.classList.remove('hidden');
this.listContainer.classList.add('hidden');
showLoading(this.editContainer);
const resp = await window.$http.get(`/attachments/edit/${id}`);
this.editContainer.innerHTML = resp.data;
window.components.init(this.editContainer);
}
stopEdit() {
this.editContainer.classList.add('hidden');
this.listContainer.classList.remove('hidden');
}
}
export default Attachments;

View file

@ -9,11 +9,15 @@ class Dropzone {
setup() {
this.container = this.$el;
this.url = this.$opts.url;
this.successMessage = this.$opts.successMessage;
this.removeMessage = this.$opts.removeMessage;
this.uploadLimitMessage = this.$opts.uploadLimitMessage;
this.timeoutMessage = this.$opts.timeoutMessage;
const _this = this;
this.dz = new DropZoneLib(this.container, {
addRemoveLinks: true,
dictRemoveFile: window.trans('components.image_upload_remove'),
dictRemoveFile: this.removeMessage,
timeout: Number(window.uploadTimeout) || 60000,
maxFilesize: Number(window.uploadLimit) || 256,
url: this.url,
@ -32,15 +36,20 @@ class Dropzone {
const token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
xhr.ontimeout = function (e) {
xhr.ontimeout = (e) => {
this.dz.emit('complete', file);
this.dz.emit('error', file, window.trans('errors.file_upload_timeout'));
this.dz.emit('error', file, this.timeoutMessage);
}
}
onSuccess(file, data) {
this.container.dispatchEvent(new Event('dropzone'))
this.$emit('success', {file, data});
if (this.successMessage) {
window.$events.emit('success', this.successMessage);
}
fadeOut(file.previewElement, 800, () => {
this.dz.removeFile(file);
});
@ -55,7 +64,7 @@ class Dropzone {
}
if (xhr && xhr.status === 413) {
setMessage(window.trans('errors.server_upload_limit'))
setMessage(this.uploadLimitMessage);
} else if (errorMessage.file) {
setMessage(errorMessage.file);
}

View file

@ -0,0 +1,29 @@
import {onSelect} from "../services/dom";
/**
* EventEmitSelect
* Component will simply emit an event when selected.
*
* Has one required option: "name".
* A name of "hello" will emit a component DOM event of
* "event-emit-select-name"
*
* All options will be set as the "detail" of the event with
* their values included.
*
* @extends {Component}
*/
class EventEmitSelect {
setup() {
this.container = this.$el;
this.name = this.$opts.name;
onSelect(this.$el, () => {
this.$emit(this.name, this.$opts);
});
}
}
export default EventEmitSelect;

View file

@ -53,6 +53,14 @@ export function onEnterPress(elements, callback) {
if (!Array.isArray(elements)) {
elements = [elements];
}
const listener = event => {
if (event.key === 'Enter') {
callback(event);
}
}
elements.forEach(e => e.addEventListener('keypress', listener));
}
/**
@ -89,4 +97,13 @@ export function findText(selector, text) {
}
}
return null;
}
/**
* Show a loading indicator in the given element.
* This will effectively clear the element.
* @param {Element} element
*/
export function showLoading(element) {
element.innerHTML = `<div class="loading-container"><div></div><div></div><div></div></div>`;
}

View file

@ -67,11 +67,20 @@ async function dataRequest(method, url, data = null) {
body: data,
};
// Send data as JSON if a plain object
if (typeof data === 'object' && !(data instanceof FormData)) {
options.headers = {'Content-Type': 'application/json'};
options.body = JSON.stringify(data);
}
// Ensure FormData instances are sent over POST
// Since Laravel does not read multipart/form-data from other types
// of request. Hence the addition of the magic _method value.
if (data instanceof FormData && method !== 'post') {
data.append('_method', method);
options.method = 'post';
}
return request(url, options)
}
@ -109,7 +118,7 @@ async function request(url, options = {}) {
const response = await fetch(url, options);
const content = await getResponseContent(response);
return {
const returnData = {
data: content,
headers: response.headers,
redirected: response.redirected,
@ -117,7 +126,13 @@ async function request(url, options = {}) {
statusText: response.statusText,
url: response.url,
original: response,
};
if (!response.ok) {
throw returnData;
}
return returnData;
}
/**

View file

@ -1,142 +0,0 @@
import draggable from "vuedraggable";
import dropzone from "./components/dropzone";
function mounted() {
this.pageId = this.$el.getAttribute('page-id');
this.file = this.newFile();
this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => {
this.files = resp.data;
}).catch(err => {
this.checkValidationErrors('get', err);
});
}
let data = {
pageId: null,
files: [],
fileToEdit: null,
file: {},
tab: 'list',
editTab: 'file',
errors: {link: {}, edit: {}, delete: {}}
};
const components = {dropzone, draggable};
let methods = {
newFile() {
return {page_id: this.pageId};
},
getFileUrl(file) {
if (file.external && file.path.indexOf('http') !== 0) {
return file.path;
}
return window.baseUrl(`/attachments/${file.id}`);
},
fileSortUpdate() {
this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => {
this.$events.emit('success', resp.data.message);
}).catch(err => {
this.checkValidationErrors('sort', err);
});
},
startEdit(file) {
this.fileToEdit = Object.assign({}, file);
this.fileToEdit.link = file.external ? file.path : '';
this.editTab = file.external ? 'link' : 'file';
},
deleteFile(file) {
if (!file.deleting) {
return this.$set(file, 'deleting', true);
}
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
this.$events.emit('success', resp.data.message);
this.files.splice(this.files.indexOf(file), 1);
}).catch(err => {
this.checkValidationErrors('delete', err)
});
},
uploadSuccess(upload) {
this.files.push(upload.data);
this.$events.emit('success', trans('entities.attachments_file_uploaded'));
},
uploadSuccessUpdate(upload) {
let fileIndex = this.filesIndex(upload.data);
if (fileIndex === -1) {
this.files.push(upload.data)
} else {
this.files.splice(fileIndex, 1, upload.data);
}
if (this.fileToEdit && this.fileToEdit.id === upload.data.id) {
this.fileToEdit = Object.assign({}, upload.data);
}
this.$events.emit('success', trans('entities.attachments_file_updated'));
},
checkValidationErrors(groupName, err) {
if (typeof err.response.data === "undefined" && typeof err.response.data === "undefined") return;
this.errors[groupName] = err.response.data;
},
getUploadUrl(file) {
let url = window.baseUrl(`/attachments/upload`);
if (typeof file !== 'undefined') url += `/${file.id}`;
return url;
},
cancelEdit() {
this.fileToEdit = null;
},
attachNewLink(file) {
file.uploaded_to = this.pageId;
this.errors.link = {};
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
this.files.push(resp.data);
this.file = this.newFile();
this.$events.emit('success', trans('entities.attachments_link_attached'));
}).catch(err => {
this.checkValidationErrors('link', err);
});
},
updateFile(file) {
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
let search = this.filesIndex(resp.data);
if (search === -1) {
this.files.push(resp.data);
} else {
this.files.splice(search, 1, resp.data);
}
if (this.fileToEdit && !file.external) this.fileToEdit.link = '';
this.fileToEdit = false;
this.$events.emit('success', trans('entities.attachments_updated_success'));
}).catch(err => {
this.checkValidationErrors('edit', err);
});
},
filesIndex(file) {
for (let i = 0, len = this.files.length; i < len; i++) {
if (this.files[i].id === file.id) return i;
}
return -1;
}
};
export default {
data, methods, mounted, components,
};

View file

@ -5,12 +5,10 @@ function exists(id) {
}
import imageManager from "./image-manager";
import attachmentManager from "./attachment-manager";
import pageEditor from "./page-editor";
let vueMapping = {
'image-manager': imageManager,
'attachment-manager': attachmentManager,
'page-editor': pageEditor,
};

View file

@ -46,7 +46,6 @@ return [
'file_upload_timeout' => 'The file upload has timed out.',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
'attachment_not_found' => 'Attachment not found',
// Pages

View file

@ -0,0 +1,8 @@
@foreach($attachments as $attachment)
<div class="attachment icon-list">
<a class="icon-list-item py-xs" href="{{ $attachment->getUrl() }}" @if($attachment->external) target="_blank" @endif>
<span class="icon">@icon($attachment->external ? 'export' : 'file')</span>
<span>{{ $attachment->name }}</span>
</a>
</div>
@endforeach

View file

@ -0,0 +1,47 @@
<div component="ajax-form"
option:ajax-form:url="/attachments/{{ $attachment->id }}"
option:ajax-form:method="put"
option:ajax-form:success-message="{{ trans('entities.attachments_updated_success') }}">
<h5>{{ trans('entities.attachments_edit_file') }}</h5>
<div class="form-group">
<label for="attachment_edit_name">{{ trans('entities.attachments_edit_file_name') }}</label>
<input type="text" id="attachment_edit_name"
name="attachment_edit_name"
value="{{ $attachment_edit_name ?? $attachment->name ?? '' }}"
placeholder="{{ trans('entities.attachments_edit_file_name') }}">
@if($errors->has('attachment_edit_name'))
<div class="text-neg text-small">{{ $errors->first('attachment_edit_name') }}</div>
@endif
</div>
<div component="tabs" class="tab-container">
<div class="nav-tabs">
<button refs="tabs@toggleFile" type="button" class="tab-item {{ $attachment->external ? '' : 'selected' }}">{{ trans('entities.attachments_upload') }}</button>
<button refs="tabs@toggleLink" type="button" class="tab-item {{ $attachment->external ? 'selected' : '' }}">{{ trans('entities.attachments_set_link') }}</button>
</div>
<div refs="tabs@contentFile" class="mb-m {{ $attachment->external ? 'hidden' : '' }}">
@include('components.dropzone', [
'placeholder' => trans('entities.attachments_edit_drop_upload'),
'url' => url('/attachments/upload/' . $attachment->id),
'successMessage' => trans('entities.attachments_file_updated'),
])
</div>
<div refs="tabs@contentLink" class="{{ $attachment->external ? '' : 'hidden' }}">
<div class="form-group">
<label for="attachment_edit_url">{{ trans('entities.attachments_link_url') }}</label>
<input type="text" id="attachment_edit_url"
name="attachment_edit_url"
value="{{ $attachment_edit_url ?? ($attachment->external ? $attachment->path : '') }}"
placeholder="{{ trans('entities.attachment_link') }}">
@if($errors->has('attachment_edit_url'))
<div class="text-neg text-small">{{ $errors->first('attachment_edit_url') }}</div>
@endif
</div>
</div>
</div>
<button component="event-emit-select"
option:event-emit-select:name="edit-back" type="button" class="button outline">{{ trans('common.back') }}</button>
<button refs="ajax-form@submit" type="button" class="button">{{ trans('common.save') }}</button>
</div>

View file

@ -0,0 +1,27 @@
{{--
@pageId
--}}
<div component="ajax-form"
option:ajax-form:url="/attachments/link"
option:ajax-form:method="post"
option:ajax-form:success-message="{{ trans('entities.attachments_link_attached') }}">
<input type="hidden" name="attachment_link_uploaded_to" value="{{ $pageId }}">
<p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
<div class="form-group">
<label for="attachment_link_name">{{ trans('entities.attachments_link_name') }}</label>
<input name="attachment_link_name" id="attachment_link_name" type="text" placeholder="{{ trans('entities.attachments_link_name') }}" value="{{ $attachment_link_name ?? '' }}">
@if($errors->has('attachment_link_name'))
<div class="text-neg text-small">{{ $errors->first('attachment_link_name') }}</div>
@endif
</div>
<div class="form-group">
<label for="attachment_link_url">{{ trans('entities.attachments_link_url') }}</label>
<input name="attachment_link_url" id="attachment_link_url" type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" value="{{ $attachment_link_url ?? '' }}">
@if($errors->has('attachment_link_url'))
<div class="text-neg text-small">{{ $errors->first('attachment_link_url') }}</div>
@endif
</div>
<button refs="ajax-form@submit"
type="button"
class="button">{{ trans('entities.attach') }}</button>
</div>

View file

@ -9,7 +9,11 @@
<a href="{{ $attachment->getUrl() }}" target="_blank">{{ $attachment->name }}</a>
</div>
<div class="flex-fill justify-flex-end">
<button type="button" class="drag-card-action text-center text-primary">@icon('edit')</button>
<button component="event-emit-select"
option:event-emit-select:name="edit"
option:event-emit-select:id="{{ $attachment->id }}"
type="button"
class="drag-card-action text-center text-primary">@icon('edit')</button>
<div component="dropdown" class="flex-fill relative">
<button refs="dropdown@toggle" type="button" class="drag-card-action text-center text-neg">@icon('close')</button>
<div refs="dropdown@menu" class="dropdown-menu">

View file

@ -0,0 +1,39 @@
<div style="display: block;" toolbox-tab-content="files"
component="attachments"
option:attachments:page-id="{{ $page->id ?? 0 }}">
<h4>{{ trans('entities.attachments') }}</h4>
<div class="px-l files">
<div refs="attachments@listContainer">
<p class="text-muted small">{{ trans('entities.attachments_explain') }} <span class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
<div component="tabs" refs="attachments@mainTabs" class="tab-container">
<div class="nav-tabs">
<button refs="tabs@toggleItems" type="button" class="selected tab-item">{{ trans('entities.attachments_items') }}</button>
<button refs="tabs@toggleUpload" type="button" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
<button refs="tabs@toggleLinks" type="button" class="tab-item">{{ trans('entities.attachments_link') }}</button>
</div>
<div refs="tabs@contentItems attachments@list">
@include('attachments.manager-list', ['attachments' => $page->attachments->all()])
</div>
<div refs="tabs@contentUpload" class="hidden">
@include('components.dropzone', [
'placeholder' => trans('entities.attachments_dropzone'),
'url' => url('/attachments/upload?uploaded_to=' . $page->id),
'successMessage' => trans('entities.attachments_file_uploaded'),
])
</div>
<div refs="tabs@contentLinks" class="hidden">
@include('attachments.manager-link-form', ['pageId' => $page->id])
</div>
</div>
</div>
<div refs="attachments@editContainer" class="hidden">
</div>
</div>
</div>

View file

@ -66,6 +66,4 @@
</div>
</div>
@include('pages.attachment-manager', ['page' => \BookStack\Entities\Page::first()])
@stop

View file

@ -1,9 +1,15 @@
{{--
@url - URL to upload to.
@placeholder - Placeholder text
@successMessage
--}}
<div component="dropzone"
option:dropzone:url="{{ $url }}"
option:dropzone:success-message="{{ $successMessage ?? '' }}"
option:dropzone:remove-message="{{ trans('components.image_upload_remove') }}"
option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
option:dropzone:timeout-message="{{ trans('errors.file_upload_timeout') }}"
class="dropzone-container text-center">
<button type="button" class="dz-message">{{ $placeholder }}</button>
</div>

View file

@ -1,92 +0,0 @@
<div style="display: block;" toolbox-tab-content="files"
component="attachments"
option:attachments:page-id="{{ $page->id ?? 0 }}">
@exposeTranslations([
'entities.attachments_file_uploaded',
'entities.attachments_file_updated',
'entities.attachments_link_attached',
'entities.attachments_updated_success',
'errors.server_upload_limit',
'components.image_upload_remove',
'components.file_upload_timeout',
])
<h4>{{ trans('entities.attachments') }}</h4>
<div class="px-l files">
<div id="file-list">
<p class="text-muted small">{{ trans('entities.attachments_explain') }} <span class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
<div component="tabs" refs="attachments@mainTabs" class="tab-container">
<div class="nav-tabs">
<button refs="tabs@toggleItems" type="button" class="selected tab-item">{{ trans('entities.attachments_items') }}</button>
<button refs="tabs@toggleUpload" type="button" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
<button refs="tabs@toggleLinks" type="button" class="tab-item">{{ trans('entities.attachments_link') }}</button>
</div>
<div refs="tabs@contentItems attachments@list">
@include('pages.attachment-list', ['attachments' => $page->attachments->all()])
</div>
<div refs="tabs@contentUpload" class="hiden">
@include('components.dropzone', [
'placeholder' => trans('entities.attachments_dropzone'),
'url' => url('/attachments/upload?uploaded_to=' . $page->id)
])
</div>
<div refs="tabs@contentLinks" class="hidden">
<p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
<div class="form-group">
<label for="attachment_link_name">{{ trans('entities.attachments_link_name') }}</label>
<input name="attachment_link_name" id="attachment_link_name" type="text" placeholder="{{ trans('entities.attachments_link_name') }}">
<p class="small text-neg"></p>
</div>
<div class="form-group">
<label for="attachment_link_url">{{ trans('entities.attachments_link_url') }}</label>
<input name="attachment_link_url" id="attachment_link_url" type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}">
<p class="small text-neg"></p>
</div>
<button class="button">{{ trans('entities.attach') }}</button>
</div>
</div>
</div>
<div refs="attachments@editContainer" class="hidden">
<h5>{{ trans('entities.attachments_edit_file') }}</h5>
<div class="form-group">
<label for="attachment-name-edit">{{ trans('entities.attachments_edit_file_name') }}</label>
<input type="text" id="attachment-name-edit"
name="attachment_name"
placeholder="{{ trans('entities.attachments_edit_file_name') }}">
<p class="small text-neg"></p>
</div>
<div component="tabs" class="tab-container">
<div class="nav-tabs">
<button refs="tabs@toggleFile" type="button" class="tab-item selected">{{ trans('entities.attachments_upload') }}</button>
<button refs="tabs@toggleLink" type="button" class="tab-item">{{ trans('entities.attachments_set_link') }}</button>
</div>
<div refs="tabs@contentFile">
@include('components.dropzone', [
'placeholder' => trans('entities.attachments_edit_drop_upload'),
'url' => url('/attachments')
])
<dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone>
<br>
</div>
<div refs="tabs@contentLink" class="hidden">
<div class="form-group">
<label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
<input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" v-model="fileToEdit.link">
<p class="small text-neg"></p>
</div>
</div>
</div>
<button type="button" class="button outline">{{ trans('common.back') }}</button>
<button class="button">{{ trans('common.save') }}</button>
</div>
</div>
</div>

View file

@ -17,7 +17,7 @@
</div>
@if(userCan('attachment-create-all'))
@include('pages.attachment-manager', ['page' => $page])
@include('attachments.manager', ['page' => $page])
@endif
<div toolbox-tab-content="templates">

View file

@ -37,14 +37,7 @@
<div id="page-attachments" class="mb-l">
<h5>{{ trans('entities.pages_attachments') }}</h5>
<div class="body">
@foreach($page->attachments as $attachment)
<div class="attachment icon-list">
<a class="icon-list-item py-xs" href="{{ $attachment->getUrl() }}" @if($attachment->external) target="_blank" @endif>
<span class="icon">@icon($attachment->external ? 'export' : 'file')</span>
<span>{{ $attachment->name }}</span>
</a>
</div>
@endforeach
@include('attachments.list', ['attachments' => $page->attachments])
</div>
</div>
@endif

View file

@ -124,6 +124,7 @@ Route::group(['middleware' => 'auth'], function () {
Route::post('/attachments/upload/{id}', 'AttachmentController@uploadUpdate');
Route::post('/attachments/link', 'AttachmentController@attachLink');
Route::put('/attachments/{id}', 'AttachmentController@update');
Route::get('/attachments/edit/{id}', 'AttachmentController@getUpdateForm');
Route::get('/attachments/get/page/{pageId}', 'AttachmentController@listForPage');
Route::put('/attachments/sort/page/{pageId}', 'AttachmentController@sortForPage');
Route::delete('/attachments/{id}', 'AttachmentController@delete');