Finished breakdown of attachment vue into components
This commit is contained in:
parent
14b6cd1091
commit
d41452f39c
24 changed files with 371 additions and 321 deletions
|
@ -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
8
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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/)
|
||||
|
|
58
resources/js/components/ajax-form.js
Normal file
58
resources/js/components/ajax-form.js
Normal 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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
29
resources/js/components/event-emit-select.js
Normal file
29
resources/js/components/event-emit-select.js
Normal 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;
|
|
@ -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>`;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
8
resources/views/attachments/list.blade.php
Normal file
8
resources/views/attachments/list.blade.php
Normal 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
|
47
resources/views/attachments/manager-edit-form.blade.php
Normal file
47
resources/views/attachments/manager-edit-form.blade.php
Normal 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>
|
27
resources/views/attachments/manager-link-form.blade.php
Normal file
27
resources/views/attachments/manager-link-form.blade.php
Normal 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>
|
|
@ -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">
|
39
resources/views/attachments/manager.blade.php
Normal file
39
resources/views/attachments/manager.blade.php
Normal 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>
|
|
@ -66,6 +66,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@include('pages.attachment-manager', ['page' => \BookStack\Entities\Page::first()])
|
||||
|
||||
@stop
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue