From 8710c099176f95890e9e1d07881090d1b39c5e93 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 25 Jul 2025 08:31:33 -0500 Subject: [PATCH 1/2] Implement Toast utility for consistent toast notifications across the application --- inventory/static/js/csv.js | 4 +- inventory/static/js/toast.js | 70 +++++++++++++++++++ inventory/static/js/widget.js | 65 ++--------------- .../fragments/_dropdown_fragment.html | 1 + inventory/templates/inventory.html | 10 +-- inventory/templates/layout.html | 8 +-- inventory/templates/settings.html | 4 +- inventory/templates/user.html | 2 +- inventory/templates/worklog.html | 10 +-- 9 files changed, 92 insertions(+), 82 deletions(-) create mode 100644 inventory/static/js/toast.js diff --git a/inventory/static/js/csv.js b/inventory/static/js/csv.js index 9ccb765..3b22e12 100644 --- a/inventory/static/js/csv.js +++ b/inventory/static/js/csv.js @@ -25,9 +25,9 @@ async function export_csv(ids, csv_route, filename=`${csv_route}_export.csv`) { URL.revokeObjectURL(url); } else { - renderToast({ message: `Export failed: ${result.error}`, type: 'danger' }); + Toast.renderToast({ message: `Export failed: ${result.error}`, type: 'danger' }); } } catch (err) { - renderToast({ message: `Export failed: ${err}`, type: 'danger' }); + Toast.renderToast({ message: `Export failed: ${err}`, type: 'danger' }); } } \ No newline at end of file diff --git a/inventory/static/js/toast.js b/inventory/static/js/toast.js new file mode 100644 index 0000000..c6797d6 --- /dev/null +++ b/inventory/static/js/toast.js @@ -0,0 +1,70 @@ +document.addEventListener("DOMContentLoaded", () => { + const toastData = localStorage.getItem("toastMessage"); + if (toastData) { + const { message, type } = JSON.parse(toastData); + Toast.renderToast({ message, type }); + localStorage.removeItem("toastMessage"); + } +}); + +const Toast = (() => { + const ToastConfig = { + containerId: 'toast-container', + positionClasses: 'toast-container position-fixed bottom-0 end-0 p-3', + defaultType: 'info', + defaultTimeout: 3000 + }; + + function updateToastConfig(overrides = {}) { + Object.assign(ToastConfig, overrides); + } + + function renderToast({ message, type = ToastConfig.defaultType, timeout = ToastConfig.defaultTimeout }) { + if (!message) { + console.warn('renderToast was called without a message.'); + return; + } + + // Auto-create the toast container if it doesn't exist + let container = document.getElementById(ToastConfig.containerId); + if (!container) { + container = document.createElement('div'); + container.id = ToastConfig.containerId; + container.className = ToastConfig.positionClasses; + document.body.appendChild(container); + } + + const toastId = `toast-${Date.now()}`; + const wrapper = document.createElement('div'); + wrapper.innerHTML = ` + + `; + + const toastElement = wrapper.firstElementChild; + container.appendChild(toastElement); + + const toast = new bootstrap.Toast(toastElement, { delay: timeout }); + toast.show(); + + toastElement.addEventListener('hidden.bs.toast', () => { + toastElement.remove(); + + // Clean up container if empty + if (container.children.length === 0) { + container.remove(); + } + }); + } + + return { + updateToastConfig, + renderToast + }; +})(); \ No newline at end of file diff --git a/inventory/static/js/widget.js b/inventory/static/js/widget.js index a837330..5f5dcb4 100644 --- a/inventory/static/js/widget.js +++ b/inventory/static/js/widget.js @@ -1,58 +1,3 @@ -const ToastConfig = { - containerId: 'toast-container', - positionClasses: 'toast-container position-fixed bottom-0 end-0 p-3', - defaultType: 'info', - defaultTimeout: 3000 -}; - -function updateToastConfig(overrides = {}) { - Object.assign(ToastConfig, overrides); -} - -function renderToast({ message, type = ToastConfig.defaultType, timeout = ToastConfig.defaultTimeout }) { - if (!message) { - console.warn('renderToast was called without a message.'); - return; - } - - // Auto-create the toast container if it doesn't exist - let container = document.getElementById(ToastConfig.containerId); - if (!container) { - container = document.createElement('div'); - container.id = ToastConfig.containerId; - container.className = ToastConfig.positionClasses; - document.body.appendChild(container); - } - - const toastId = `toast-${Date.now()}`; - const wrapper = document.createElement('div'); - wrapper.innerHTML = ` - - `; - - const toastElement = wrapper.firstElementChild; - container.appendChild(toastElement); - - const toast = new bootstrap.Toast(toastElement, { delay: timeout }); - toast.show(); - - toastElement.addEventListener('hidden.bs.toast', () => { - toastElement.remove(); - - // Clean up container if empty - if (container.children.length === 0) { - container.remove(); - } - }); -} - const ImageWidget = (() => { function submitImageUpload(id) { const form = document.getElementById(`image-upload-form-${id}`); @@ -75,11 +20,11 @@ const ImageWidget = (() => { } return response.json(); }).then(data => { - renderToast({ message: `Image uploaded.`, type: "success" }); + Toast.renderToast({ message: `Image uploaded.`, type: "success" }); location.reload(); }).catch(err => { const msg = typeof err === "object" && err.error ? err.error : err.toString(); - renderToast({ message: `Upload failed: ${msg}`, type: "danger" }); + Toast.renderToast({ message: `Upload failed: ${msg}`, type: "danger" }); }); } @@ -90,13 +35,13 @@ const ImageWidget = (() => { method: "DELETE" }).then(response => response.json()).then(data => { if (data.success) { - renderToast({ message: "Image deleted.", type: "success" }); + Toast.renderToast({ message: "Image deleted.", type: "success" }); location.reload(); // Update view } else { - renderToast({ message: `Failed to delete: ${data.error}`, type: "danger" }); + Toast.renderToast({ message: `Failed to delete: ${data.error}`, type: "danger" }); } }).catch(err => { - renderToast({ message: `Error deleting image: ${err}`, type: "danger" }); + Toast.renderToast({ message: `Error deleting image: ${err}`, type: "danger" }); }); } diff --git a/inventory/templates/fragments/_dropdown_fragment.html b/inventory/templates/fragments/_dropdown_fragment.html index 4b1b9f8..9d76e46 100644 --- a/inventory/templates/fragments/_dropdown_fragment.html +++ b/inventory/templates/fragments/_dropdown_fragment.html @@ -33,6 +33,7 @@ function {{ id }}SetButton(id, identifier) { const button = document.getElementById("{{ id }}Button"); const input = document.getElementById("{{ id }}"); + button.dataset.invValue = id; button.textContent = identifier; input.value = id; diff --git a/inventory/templates/inventory.html b/inventory/templates/inventory.html index a0014cb..b33b2b0 100644 --- a/inventory/templates/inventory.html +++ b/inventory/templates/inventory.html @@ -46,18 +46,18 @@ window.location.href = `/inventory_item/${result.id}`; } else { - renderToast({ message: `Error: ${result.error}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${result.error}`, type: "danger" }); } } catch (err) { console.error(err); - renderToast({ message: `Error: ${err}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${err}`, type: "danger" }); } {% endset %} {% set deleteLogic %} const id = document.querySelector("#inventoryId").value; if (!id || id === "None") { - renderToast({ message: "No item ID found to delete.", type: "danger" }); + Toast.renderToast({ message: "No item ID found to delete.", type: "danger" }); return; } @@ -80,11 +80,11 @@ window.location.href = "/inventory"; } else { - renderToast({ message: `Error: ${result.error}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${result.error}`, type: "danger" }); } } catch (err) { console.error(err); - renderToast({ message: `Error: ${err}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${err}`, type: "danger" }); } {% endset %} {% set buttonBar %} diff --git a/inventory/templates/layout.html b/inventory/templates/layout.html index f4b911f..bed056b 100644 --- a/inventory/templates/layout.html +++ b/inventory/templates/layout.html @@ -76,6 +76,7 @@ crossorigin="anonymous"> + diff --git a/inventory/templates/settings.html b/inventory/templates/settings.html index 78bb9c6..36d8069 100644 --- a/inventory/templates/settings.html +++ b/inventory/templates/settings.html @@ -90,10 +90,10 @@ } const data = await response.json(); - renderToast({ message: 'Settings updated successfully.', type: 'success' }); + Toast.renderToast({ message: 'Settings updated successfully.', type: 'success' }); } catch (err) { - renderToast({ message: `Failed to update settings, ${err}`, type: 'danger' }); + Toast.renderToast({ message: `Failed to update settings, ${err}`, type: 'danger' }); } {% endset %} {{ toolbars.render_toolbar( diff --git a/inventory/templates/user.html b/inventory/templates/user.html index c2ccab2..96a0a83 100644 --- a/inventory/templates/user.html +++ b/inventory/templates/user.html @@ -40,7 +40,7 @@ window.location.href = `/user/${result.id}`; } else { - renderToast({ message: `Error: ${result.error}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${result.error}`, type: "danger" }); } } catch (err) { console.error(err); diff --git a/inventory/templates/worklog.html b/inventory/templates/worklog.html index f84d6b5..8ce580d 100644 --- a/inventory/templates/worklog.html +++ b/inventory/templates/worklog.html @@ -52,18 +52,18 @@ window.location.href = `/worklog/${result.id}`; } else { - renderToast({ message: `Error: ${result.error}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${result.error}`, type: "danger" }); } } catch (err) { console.error(err) - renderToast({ message: `Error: ${err}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${err}`, type: "danger" }); } {% endset %} {% set deleteLogic %} const id = document.querySelector("#logId").value; if (!id || id === "None") { - renderToast({ message: "No item ID found to delete.", type: "danger" }); + Toast.renderToast({ message: "No item ID found to delete.", type: "danger" }); return; } @@ -86,10 +86,10 @@ window.location.href = "/worklog"; } else { - renderToast({ message: `Error: ${result.error}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${result.error}`, type: "danger" }); } } catch (err) { - renderToast({ message: `Error: ${err}`, type: "danger" }); + Toast.renderToast({ message: `Error: ${err}`, type: "danger" }); } {% endset %} {% set iconBar %} From d488324c5074b3db5d5b4991dc3fa55850097b1e Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 25 Jul 2025 08:42:07 -0500 Subject: [PATCH 2/2] Add ImageWidget for image upload and deletion functionality with toast notifications --- inventory/static/js/image.js | 52 ++++++++++++++++++ inventory/static/js/widget.js | 53 ------------------- .../templates/fragments/_image_fragment.html | 6 +-- inventory/templates/layout.html | 1 + 4 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 inventory/static/js/image.js diff --git a/inventory/static/js/image.js b/inventory/static/js/image.js new file mode 100644 index 0000000..57653cc --- /dev/null +++ b/inventory/static/js/image.js @@ -0,0 +1,52 @@ +const ImageWidget = (() => { + function submitImageUpload(id) { + const form = document.getElementById(`image-upload-form-${id}`); + const formData = new FormData(form); + + fetch("/api/images", { + method: "POST", + body: formData + }).then(async response => { + if (!response.ok) { + // Try to parse JSON, fallback to text + const contentType = response.headers.get("Content-Type") || ""; + let errorDetails; + if (contentType.includes("application/json")) { + errorDetails = await response.json(); + } else { + errorDetails = { error: await response.text() }; + } + throw errorDetails; + } + return response.json(); + }).then(data => { + Toast.renderToast({ message: `Image uploaded.`, type: "success" }); + location.reload(); + }).catch(err => { + const msg = typeof err === "object" && err.error ? err.error : err.toString(); + Toast.renderToast({ message: `Upload failed: ${msg}`, type: "danger" }); + }); + } + + function deleteImage(inventoryId, imageId) { + if (!confirm("Are you sure you want to delete this image?")) return; + + fetch(`/api/images/${imageId}`, { + method: "DELETE" + }).then(response => response.json()).then(data => { + if (data.success) { + Toast.renderToast({ message: "Image deleted.", type: "success" }); + location.reload(); // Update view + } else { + Toast.renderToast({ message: `Failed to delete: ${data.error}`, type: "danger" }); + } + }).catch(err => { + Toast.renderToast({ message: `Error deleting image: ${err}`, type: "danger" }); + }); + } + + return { + submitImageUpload, + deleteImage + } +})(); \ No newline at end of file diff --git a/inventory/static/js/widget.js b/inventory/static/js/widget.js index 5f5dcb4..a453ad6 100644 --- a/inventory/static/js/widget.js +++ b/inventory/static/js/widget.js @@ -1,56 +1,3 @@ -const ImageWidget = (() => { - function submitImageUpload(id) { - const form = document.getElementById(`image-upload-form-${id}`); - const formData = new FormData(form); - - fetch("/api/images", { - method: "POST", - body: formData - }).then(async response => { - if (!response.ok) { - // Try to parse JSON, fallback to text - const contentType = response.headers.get("Content-Type") || ""; - let errorDetails; - if (contentType.includes("application/json")) { - errorDetails = await response.json(); - } else { - errorDetails = { error: await response.text() }; - } - throw errorDetails; - } - return response.json(); - }).then(data => { - Toast.renderToast({ message: `Image uploaded.`, type: "success" }); - location.reload(); - }).catch(err => { - const msg = typeof err === "object" && err.error ? err.error : err.toString(); - Toast.renderToast({ message: `Upload failed: ${msg}`, type: "danger" }); - }); - } - - function deleteImage(inventoryId, imageId) { - if (!confirm("Are you sure you want to delete this image?")) return; - - fetch(`/api/images/${imageId}`, { - method: "DELETE" - }).then(response => response.json()).then(data => { - if (data.success) { - Toast.renderToast({ message: "Image deleted.", type: "success" }); - location.reload(); // Update view - } else { - Toast.renderToast({ message: `Failed to delete: ${data.error}`, type: "danger" }); - } - }).catch(err => { - Toast.renderToast({ message: `Error deleting image: ${err}`, type: "danger" }); - }); - } - - return { - submitImageUpload, - deleteImage - } -})(); - const EditorWidget = (() => { let tempIdCounter = 1; diff --git a/inventory/templates/fragments/_image_fragment.html b/inventory/templates/fragments/_image_fragment.html index 4da308c..6ff57cb 100644 --- a/inventory/templates/fragments/_image_fragment.html +++ b/inventory/templates/fragments/_image_fragment.html @@ -6,7 +6,7 @@ {% if image %} Image of ID {{ id }} -