]> BookStack Code Mirror - bookstack/commitdiff
Finished breakdown of attachment vue into components
authorDan Brown <redacted>
Sat, 4 Jul 2020 15:53:02 +0000 (16:53 +0100)
committerDan Brown <redacted>
Sat, 4 Jul 2020 15:53:02 +0000 (16:53 +0100)
24 files changed:
app/Http/Controllers/AttachmentController.php
package-lock.json
package.json
readme.md
resources/js/components/ajax-form.js [new file with mode: 0644]
resources/js/components/attachments.js
resources/js/components/dropzone.js
resources/js/components/event-emit-select.js [new file with mode: 0644]
resources/js/services/dom.js
resources/js/services/http.js
resources/js/vues/attachment-manager.js [deleted file]
resources/js/vues/vues.js
resources/lang/en/errors.php
resources/views/attachments/list.blade.php [new file with mode: 0644]
resources/views/attachments/manager-edit-form.blade.php [new file with mode: 0644]
resources/views/attachments/manager-link-form.blade.php [new file with mode: 0644]
resources/views/attachments/manager-list.blade.php [moved from resources/views/pages/attachment-list.blade.php with 80% similarity]
resources/views/attachments/manager.blade.php [new file with mode: 0644]
resources/views/common/home.blade.php
resources/views/components/dropzone.blade.php
resources/views/pages/attachment-manager.blade.php [deleted file]
resources/views/pages/editor-toolbox.blade.php
resources/views/pages/show.blade.php
routes/web.php

index f209d6a9418d7aba98437f71033957a08dbe39c1..0830693bc6a73769a51a26f3cda8239d163eac8e 100644 (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);
index b601736835f32014f9af20ca3b553d0de66f9e8e..3687566ba56e28ea66a47e12e7a05af17130da01 100644 (file)
       "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",
index 7462dab92516b47a4859e5294950fd3ca0fb480e..0f1855cd92b14111a7863949dd0ca769f495614e 100644 (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"
index 2c68d094c1e5c20c3dc1167072db1c7eb1b2d5e5..c6db7a8e4ac4d827f8a18cf9e243d59988689523 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -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/)
diff --git a/resources/js/components/ajax-form.js b/resources/js/components/ajax-form.js
new file mode 100644 (file)
index 0000000..92b19dc
--- /dev/null
@@ -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;
\ No newline at end of file
index 49ba8f38877609878cbf9991afdfdab3923c4b8e..51e54054ebabba076089798a3c7591088ac18caa 100644 (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;
\ No newline at end of file
index 4b12867aa55b560e0cf68af5503689884ab7000e..5a7e29de5d40928f6766526b39330444276f03eb 100644 (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);
         }
diff --git a/resources/js/components/event-emit-select.js b/resources/js/components/event-emit-select.js
new file mode 100644 (file)
index 0000000..cf02158
--- /dev/null
@@ -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;
\ No newline at end of file
index 2a9fad8b3023b94212e88ae0a2867a3402fc3917..00b34bf345767e034b9f7b8d6794083d228a6ed0 100644 (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>`;
 }
\ No newline at end of file
index 06dac9864a4d4523987e61f4009075ea34ed0594..5b5e1c4960bd197e9095be6d4346fe2d22256bfd 100644 (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;
 }
 
 /**
diff --git a/resources/js/vues/attachment-manager.js b/resources/js/vues/attachment-manager.js
deleted file mode 100644 (file)
index 2467c64..0000000
+++ /dev/null
@@ -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,
-};
\ No newline at end of file
index 0d3817f0ed34ad0b4ed9f29ddf0d555ffbc2ae6e..d0bd529efe309a4e0878f329fb7b60159d8e21de 100644 (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,
 };
 
index 06a5285f56fc4ce11e6642549a1002b1bacae698..79024e482ed69efa633116592f9b7c83a0bcc93a 100644 (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
diff --git a/resources/views/attachments/list.blade.php b/resources/views/attachments/list.blade.php
new file mode 100644 (file)
index 0000000..8c9be82
--- /dev/null
@@ -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
\ No newline at end of file
diff --git a/resources/views/attachments/manager-edit-form.blade.php b/resources/views/attachments/manager-edit-form.blade.php
new file mode 100644 (file)
index 0000000..f3f11a0
--- /dev/null
@@ -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>
\ No newline at end of file
diff --git a/resources/views/attachments/manager-link-form.blade.php b/resources/views/attachments/manager-link-form.blade.php
new file mode 100644 (file)
index 0000000..6f22abb
--- /dev/null
@@ -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>
\ No newline at end of file
similarity index 80%
rename from resources/views/pages/attachment-list.blade.php
rename to resources/views/attachments/manager-list.blade.php
index a9801870cada12741fc26b4ca2761215d447742c..06ab5f912b89e7046fbb91bb7bd3f1daa954a3f7 100644 (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">
diff --git a/resources/views/attachments/manager.blade.php b/resources/views/attachments/manager.blade.php
new file mode 100644 (file)
index 0000000..4bfa976
--- /dev/null
@@ -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>
\ No newline at end of file
index 2464b019bf307be45ffeb7777f8b98495aa06633..2631f1a57ed878b01ad5099f7a539e07169eba58 100644 (file)
@@ -66,6 +66,4 @@
         </div>
     </div>
 
-    @include('pages.attachment-manager', ['page' => \BookStack\Entities\Page::first()])
-
 @stop
index 22bf8aff4c8ef0eeea866c088f2d97f8bd14e694..6c5ac49298fde46c7558a9cb4c413b082334615c 100644 (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>
\ No newline at end of file
diff --git a/resources/views/pages/attachment-manager.blade.php b/resources/views/pages/attachment-manager.blade.php
deleted file mode 100644 (file)
index a86cd70..0000000
+++ /dev/null
@@ -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>
\ No newline at end of file
index 3741c9246ef982ffa75973196043e5f394fd57e3..87a9cc2de8bff90ba2036b0a312399cd41acbbb5 100644 (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">
index cfa5ee9ce08572c3225a4223f5d6ac24e3e257d6..48c88434e00bafd25cff7befb7e39e95f0f475e1 100644 (file)
         <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
index 6b7911825c57d5cce5e79dfc52d53e7ccd958665..314515fe8e1cea01516718a755305348f3f67b59 100644 (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');