From: Adam Dullage Date: Wed, 15 May 2024 18:40:33 +0000 (+0100) Subject: Add image upload capability X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=5277f4f372d64c8a83f1750ac32a7256008e938e;p=flatnotes.git Add image upload capability --- diff --git a/client/api.js b/client/api.js index 2a0ce05..16b4b5b 100644 --- a/client/api.js +++ b/client/api.js @@ -128,3 +128,18 @@ export async function getTags() { return Promise.reject(response); } } + +export async function createAttachment(file) { + try { + const formData = new FormData(); + formData.append("file", file); + const response = await api.post("/api/attachments", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + return response.data; + } catch (response) { + return Promise.reject(response); + } +} diff --git a/client/components/toastui/ToastEditor.vue b/client/components/toastui/ToastEditor.vue index fe5e9df..4048ea5 100644 --- a/client/components/toastui/ToastEditor.vue +++ b/client/components/toastui/ToastEditor.vue @@ -14,6 +14,7 @@ const props = defineProps({ type: String, default: "markdown", }, + addImageBlobHook: Function, }); const editorElement = ref(); @@ -25,6 +26,9 @@ onMounted(() => { el: editorElement.value, initialValue: props.initialValue, initialEditType: props.initialEditType, + hooks: props.addImageBlobHook + ? { addImageBlobHook: props.addImageBlobHook } + : {}, }); }); diff --git a/client/views/Note.vue b/client/views/Note.vue index 2a82121..136faa4 100644 --- a/client/views/Note.vue +++ b/client/views/Note.vue @@ -76,6 +76,7 @@ ref="toastEditor" :initialValue="note.content" :initialEditType="loadDefaultEditorMode()" + :addImageBlobHook="addImageBlobHook" /> @@ -96,6 +97,7 @@ import { useRouter } from "vue-router"; import { apiErrorHandler, + createAttachment, createNote, deleteNote, getNote, @@ -328,6 +330,59 @@ function noteSaveSuccess() { toast.add(getToastOptions("Success", "Note saved successfully.")); } +function addImageBlobHook(file, callback) { + const altTextInputValue = document.getElementById( + "toastuiAltTextInput", + )?.value; + + // Upload the image then use the callback to insert the URL into the editor + postAttachment(file).then(function (data) { + if (data) { + // If the user has entered an alt text, use it. Otherwise, use the filename returned by the API. + const altText = altTextInputValue ? altTextInputValue : data.filename; + callback(data.url, altText); + } + }); +} + +function postAttachment(file) { + // Invalid Character Validation + if (reservedFilenameCharacters.test(file.name)) { + badFilenameToast("Title"); + return; + } + + // Uploading Toast + toast.add(getToastOptions("Uploading", "Uploading attachment...")); + + // Upload the attachment + return createAttachment(file) + .then((data) => { + // Success Toast + toast.add( + getToastOptions("Success", "Attachment uploaded successfully."), + ); + return data; + }) + .catch((error) => { + if (error.response?.status === 409) { + // Note: The current implementation will append a datetime to the filename if it already exists. + // Error Toast + toast.add( + getToastOptions( + "Duplicate", + "An attachment with this filename already exists.", + true, + ), + ); + } else if (error.response?.status == 413) { + entityTooLargeToast("attachment"); + } else { + apiErrorHandler(error, toast); + } + }); +} + watch(() => props.title, init); onMounted(init); diff --git a/vite.config.js b/vite.config.js index 99645b2..34a0c65 100644 --- a/vite.config.js +++ b/vite.config.js @@ -13,6 +13,10 @@ export default defineConfig({ target: devApiUrl, changeOrigin: true, }, + "/attachments/": { + target: devApiUrl, + changeOrigin: true, + }, "/docs": { target: devApiUrl, changeOrigin: true,