[TikiWiki-commits] [Git][tikiwiki/tiki][29.x] [FIX] Extend the Element Plus fileGalUploader component ’s http-request with a...

"Merci Jacob \(@mercihabam\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <69428dc4c82fd_2a17fbb048a7@gitlab-sidekiq-low-urgency-cpu-bound-v2-78c94c7466-6pdrg.mail>

Merci Jacob pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki


Commits:
bb343387 by Merci Jacob at 2025-12-17T11:02:23+00:00
[FIX] Extend the Element Plus fileGalUploader component’s http-request with a TikiFeedback handler to catch and display server-side 'Feedback:error' messages
---
* [FIX] Extend the Element Plus fileGalUploader component’s http-request with a TikiFeedback handler to catch and display server-side 'Feedback:error' messages

See merge request tikiwiki/tiki!9231


(cherry picked from commit 2a0489d8d70f5e3f050d3bacdf866596d9adf5b4)

c4c0b22d [FIX] Extend the Element Plus fileGalUploader component’s http-request with a...

Co-authored-by: Merci Jacob <[email protected]>
- - - - -


6 changed files:

- src/js/vue-widgets/element-plus-ui/src/components/FileGalUploader/FileGalUploader.vue
- + src/js/vue-widgets/element-plus-ui/src/helpers/fileGalUploader/getUploadAjaxError.js
- + src/js/vue-widgets/element-plus-ui/src/helpers/fileGalUploader/handleTikiFeedback.js
- src/js/vue-widgets/element-plus-ui/src/tests/components/FileGalUploader.test.js
- + src/js/vue-widgets/element-plus-ui/src/tests/helpers/fileGalUploader/getUploadAjaxError.test.js
- + src/js/vue-widgets/element-plus-ui/src/tests/helpers/fileGalUploader/handleTikiFeedback.test.js


Changes:

=====================================
src/js/vue-widgets/element-plus-ui/src/components/FileGalUploader/FileGalUploader.vue
=====================================
@@ -1,9 +1,12 @@
 <script setup>
 import { UploadFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
+import { ajaxUpload as defaultHttpRequest } from 'element-plus/es/components/upload/src/ajax';
 import { ref, onMounted } from 'vue'
 import getUploadData from '../../helpers/fileGalUploader/getUploadData';
 import ConfigWrapper from '../ConfigWrapper.vue';
+import handleTikiFeedback from '../../helpers/fileGalUploader/handleTikiFeedback';
+import getUploadAjaxError from '../../helpers/fileGalUploader/getUploadAjaxError';
 
 const props = defineProps(['accept', 'maxSize', 'maxFiles', 'maxWidth', 'maxHeight', 'vimeoUrl', 'language']);
 const maxSize = JSON.parse(props.maxSize);
@@ -63,6 +66,15 @@ const copyToClipboard = (text) => {
         ElMessage.error('Failed to copy');
     });
 }
+
+const httpRequest = (option) => {
+    const xhr = defaultHttpRequest(option);
+    const originalOnError = option.onError;
+    option.onError = () => {
+        originalOnError(getUploadAjaxError(option, xhr));
+        handleTikiFeedback(xhr);
+    };
+}
 </script>
 
 <script>
@@ -93,6 +105,7 @@ export const DEFAULT_ACTION_URL = 'tiki-ajax_services.php?controller=file&action
             :action="vimeoUrl ? vimeoUrl : DEFAULT_ACTION_URL"
             :method="vimeoUrl ? 'PUT' : 'POST'"
             :data-testid="DATA_TEST_ID.UPLOAD_ELEMENT"
+            :http-request="httpRequest"
         >
             <el-icon class="el-icon--upload" :data-testid="DATA_TEST_ID.UPLOAD_ICON"><upload-filled /></el-icon>
             <div class="el-upload__text" :data-testid="DATA_TEST_ID.UPLOAD_TEXT">


=====================================
src/js/vue-widgets/element-plus-ui/src/helpers/fileGalUploader/getUploadAjaxError.js
=====================================
@@ -0,0 +1,20 @@
+import { UploadAjaxError } from "element-plus/es/components/upload/src/ajax";
+
+/**
+ * Returns an instance of UploadAjaxError with the appropriate message and status.
+ * @param {Object} option
+ * @param {XMLHttpRequest} xhr
+ * @returns
+ */
+export default function (option, xhr) {
+    let msg;
+    if (xhr.response) {
+        msg = `${xhr.response.error || xhr.response}`;
+    } else if (xhr.responseText) {
+        msg = `${xhr.responseText}`;
+    } else {
+        msg = `fail to ${option.method} ${option.action} ${xhr.status}`;
+    }
+
+    return new UploadAjaxError(msg, xhr.status, option.method, option.action);
+}


=====================================
src/js/vue-widgets/element-plus-ui/src/helpers/fileGalUploader/handleTikiFeedback.js
=====================================
@@ -0,0 +1,30 @@
+/**
+ * Shows any Tiki feedback message received from the ajax response headers.
+ * @param {XMLHttpRequest} xhr
+ */
+export default function (xhr) {
+    const feedback = xhr.getResponseHeader("X-Tiki-Feedback");
+    const tikiFeedbackElement = $("#tikifeedback");
+    if (feedback) {
+        const feedbackContent = decodeURIComponent(feedback);
+        tikiFeedbackElement.fadeIn(200, function () {
+            tikiFeedbackElement.html($($.parseHTML(feedbackContent)).filter("#tikifeedback").html());
+            tikiFeedbackElement.find("div.alert").each(function () {
+                const title = $(this).find("span.rboxtitle").text().trim();
+                const content = $(this).find("div.rboxcontent").text().trim();
+                $(this).find("span.rboxtitle").text(title);
+                $(this).find("div.rboxcontent").text(content);
+            });
+
+            placeFeedback(tikiFeedbackElement);
+        });
+    }
+
+    tikiFeedbackElement.find(".clear").on("click", function () {
+        $(tikiFeedbackElement).empty();
+        //move back to usual position and clear style attribute so subsequent feedback appears properly
+        $("div#col1").prepend(tikiFeedbackElement);
+        tikiFeedbackElement.css({ "z-index": "", position: "", top: "" });
+        return true;
+    });
+}


=====================================
src/js/vue-widgets/element-plus-ui/src/tests/components/FileGalUploader.test.js
=====================================
@@ -4,6 +4,9 @@ import FileGalUploader, { DATA_TEST_ID, DEFAULT_ACTION_URL } from "../../compone
 import { h } from "vue";
 import { ElMessage, ElUpload } from "element-plus";
 import ConfigWrapper from "../../components/ConfigWrapper.vue";
+import * as AjaxUploadHelpers from "element-plus/es/components/upload/src/ajax.mjs";
+import getUploadAjaxError from "../../helpers/fileGalUploader/getUploadAjaxError";
+import handleTikiFeedback from "../../helpers/fileGalUploader/handleTikiFeedback";
 
 vi.mock("element-plus", async (importOriginal) => {
     const actual = await importOriginal();
@@ -19,6 +22,26 @@ vi.mock("../../components/ConfigWrapper.vue", () => {
     };
 });
 
+vi.mock("element-plus/es/components/upload/src/ajax.mjs", async (importOriginal) => {
+    const actual = await importOriginal();
+    return {
+        ...actual,
+        ajaxUpload: vi.fn(),
+    };
+});
+
+vi.mock("../../helpers/fileGalUploader/getUploadAjaxError", () => {
+    return {
+        default: vi.fn(),
+    };
+});
+
+vi.mock("../../helpers/fileGalUploader/handleTikiFeedback", () => {
+    return {
+        default: vi.fn(),
+    };
+});
+
 describe("FileGalUploader", () => {
     const consoleErrorSpy = vi.spyOn(console, "error");
     const consoleWarnSpy = vi.spyOn(console, "warn");
@@ -46,6 +69,7 @@ describe("FileGalUploader", () => {
                     "auto-upload": false,
                     method: "POST",
                     headers: { accept: "application/json" },
+                    "http-request": expect.any(Function),
                 }),
                 expect.any(Object)
             );
@@ -130,6 +154,49 @@ describe("FileGalUploader", () => {
             expect(uploadValidation).toBe(false);
         });
 
+        test("correctly executes the http request function", async () => {
+            const givenFile = new File(["foo"], "foo.txt", { type: "text/plain" });
+
+            const givenProps = {
+                maxSize: "100",
+                maxFiles: "10",
+            };
+
+            render(FileGalUploader, { props: givenProps });
+
+            const httpRequestFunction = ElUpload.mock.calls[0][0]["http-request"];
+
+            const mockOption = {
+                action: DEFAULT_ACTION_URL,
+                method: "POST",
+                data: {},
+                filename: "file",
+                file: givenFile,
+                onProgress: vi.fn(),
+                onError: vi.fn(),
+                onSuccess: vi.fn(),
+            };
+
+            const mockXMLHttpRequestInstance = new XMLHttpRequest();
+            vi.spyOn(AjaxUploadHelpers, "ajaxUpload").mockReturnValueOnce(mockXMLHttpRequestInstance);
+
+            const alteredOnErrorSpy = vi.spyOn(mockOption, "onError");
+
+            const mockUploadAjaxErrorInstance = { error: "mock error" };
+            getUploadAjaxError.mockReturnValueOnce(mockUploadAjaxErrorInstance);
+
+            httpRequestFunction(mockOption);
+
+            expect(AjaxUploadHelpers.ajaxUpload).toHaveBeenCalledWith(mockOption);
+
+            mockOption.onError();
+            await waitFor(() => {
+                expect(alteredOnErrorSpy).toHaveBeenCalledWith(mockUploadAjaxErrorInstance);
+            });
+            expect(getUploadAjaxError).toHaveBeenCalledWith(mockOption, mockXMLHttpRequestInstance);
+            expect(handleTikiFeedback).toHaveBeenCalledWith(mockXMLHttpRequestInstance);
+        });
+
         test("show an error message when the upload fails", async () => {
             const givenProps = {
                 maxSize: "1",


=====================================
src/js/vue-widgets/element-plus-ui/src/tests/helpers/fileGalUploader/getUploadAjaxError.test.js
=====================================
@@ -0,0 +1,37 @@
+import { describe, test } from "vitest";
+import { UploadAjaxError } from "element-plus/es/components/upload/src/ajax";
+import getUploadAjaxError from "../../../helpers/fileGalUploader/getUploadAjaxError";
+
+describe("fileGalUploader getUploadAjaxError helper", () => {
+    test.each([
+        [{ response: { error: "Server response error" } }],
+        [{ response: "Server response string" }],
+        [{ responseText: "Server response text" }],
+        [{}],
+    ])("returns UploadAjaxError with correct message and status for xhr: %o", (mockXhr) => {
+        const mockOption = {
+            method: "POST",
+            action: "/upload",
+        };
+
+        const uploadAjaxErrorInstance = getUploadAjaxError(mockOption, {
+            ...mockXhr,
+            status: 500,
+        });
+
+        let expectedMessage;
+        if (mockXhr.response) {
+            expectedMessage = `${mockXhr.response.error || mockXhr.response}`;
+        } else if (mockXhr.responseText) {
+            expectedMessage = `${mockXhr.responseText}`;
+        } else {
+            expectedMessage = `fail to ${mockOption.method} ${mockOption.action} 500`;
+        }
+
+        expect(uploadAjaxErrorInstance).toBeInstanceOf(UploadAjaxError);
+        expect(uploadAjaxErrorInstance.message).toBe(expectedMessage);
+        expect(uploadAjaxErrorInstance.status).toBe(500);
+        expect(uploadAjaxErrorInstance.method).toBe("POST");
+        expect(uploadAjaxErrorInstance.url).toBe("/upload");
+    });
+});


=====================================
src/js/vue-widgets/element-plus-ui/src/tests/helpers/fileGalUploader/handleTikiFeedback.test.js
=====================================
@@ -0,0 +1,59 @@
+import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
+import $ from "jquery";
+import handleTikiFeedback from "../../../helpers/fileGalUploader/handleTikiFeedback";
+
+describe("fileGalUploader handleTikiFeedback helper", () => {
+    beforeAll(() => {
+        window.$ = $;
+        window.placeFeedback = vi.fn();
+        $.fn.fadeIn = vi.fn(function (duration, callback) {
+            $(this).data("mocked-fadein", duration);
+            callback.call(this);
+        });
+    });
+
+    beforeEach(() => {
+        $("body").empty();
+        const feedbackContainer = $(
+            "<div id='tikifeedback'><div class='alert'><span class='rboxtitle'> Title </span><div class='rboxcontent'> Content </div></div><button class='clear'>Clear</button></div>"
+        );
+        $("body").append(feedbackContainer);
+    });
+
+    test("places the given feedback on the page", () => {
+        const givenFeedbackHeader = encodeURIComponent(
+            "<div id='tikifeedback'><div class='alert'><span class='rboxtitle'> New Title </span><div class='rboxcontent'> New Content </div></div></div>"
+        );
+        const mockXhr = {
+            getResponseHeader: (headerName) => {
+                if (headerName === "X-Tiki-Feedback") {
+                    return givenFeedbackHeader;
+                }
+            },
+        };
+
+        handleTikiFeedback(mockXhr);
+
+        const tikiFeedbackElement = $("#tikifeedback");
+        expect(tikiFeedbackElement.data("mocked-fadein")).toBe(200);
+        expect(tikiFeedbackElement.find("span.rboxtitle").text().trim()).toBe("New Title");
+        expect(tikiFeedbackElement.find("div.rboxcontent").text().trim()).toBe("New Content");
+        expect(window.placeFeedback).toHaveBeenCalledWith(tikiFeedbackElement);
+    });
+
+    test("clears the feedback when the clear button is clicked", () => {
+        handleTikiFeedback({
+            getResponseHeader: (headerName) => null,
+        });
+
+        const tikiFeedbackElement = $("#tikifeedback");
+
+        const clearButton = tikiFeedbackElement.find("button.clear");
+        clearButton.trigger("click");
+
+        expect(tikiFeedbackElement.html()).toBe("");
+        expect(tikiFeedbackElement.css("z-index")).toBe("");
+        expect(tikiFeedbackElement.css("position")).toBe("");
+        expect(tikiFeedbackElement.css("top")).toBe("");
+    });
+});



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/bb3433878cf8fc58c9a5b73a2ae1d657c6df3d6c

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/bb3433878cf8fc58c9a5b73a2ae1d657c6df3d6c
You're receiving this email because of your account on gitlab.com.

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.