feat: enhance export and import workflows with improved error handling and notifications
This commit is contained in:
11
static/js/alerts.js
Normal file
11
static/js/alerts.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll("[data-toast-close]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const toast = button.closest(".toast");
|
||||||
|
if (toast) {
|
||||||
|
toast.classList.add("hidden");
|
||||||
|
setTimeout(() => toast.remove(), 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -39,7 +39,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const format = formData.get("format") || "csv";
|
const format = formData.get("format") || "csv";
|
||||||
|
|
||||||
const response = await fetch(submitUrl, {
|
const submitBtn = form.querySelector("button[type='submit']");
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.classList.add("loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(submitUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -50,9 +58,48 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
filters: null,
|
filters: null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
NotificationCenter.show({
|
||||||
|
message: "Network error during export.",
|
||||||
|
level: "error",
|
||||||
|
});
|
||||||
|
const errorContainer = form.querySelector("[data-export-error]");
|
||||||
|
if (errorContainer) {
|
||||||
|
errorContainer.textContent = "Network error during export.";
|
||||||
|
errorContainer.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
submitBtn?.classList.remove("loading");
|
||||||
|
submitBtn?.removeAttribute("disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
alert("Export failed. Please try again.");
|
let detail = "Export failed. Please try again.";
|
||||||
|
try {
|
||||||
|
const payload = await response.json();
|
||||||
|
if (payload?.detail) {
|
||||||
|
detail = Array.isArray(payload.detail)
|
||||||
|
? payload.detail.map((item) => item.msg || item).join("; ")
|
||||||
|
: payload.detail;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore JSON parse issues
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.show({
|
||||||
|
message: detail,
|
||||||
|
level: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorContainer = form.querySelector("[data-export-error]");
|
||||||
|
if (errorContainer) {
|
||||||
|
errorContainer.textContent = detail;
|
||||||
|
errorContainer.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn?.classList.remove("loading");
|
||||||
|
submitBtn?.removeAttribute("disabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +126,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (modal) {
|
if (modal) {
|
||||||
closeModal(modal);
|
closeModal(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationCenter.show({
|
||||||
|
message: `Export ready: ${filename}`,
|
||||||
|
level: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
submitBtn?.classList.remove("loading");
|
||||||
|
submitBtn?.removeAttribute("disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[data-export-trigger]").forEach((button) => {
|
document.querySelectorAll("[data-export-trigger]").forEach((button) => {
|
||||||
@@ -90,7 +145,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
await loadModal(dataset);
|
await loadModal(dataset);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert("Unable to open export dialog.");
|
NotificationCenter.show({
|
||||||
|
message: "Unable to open export dialog.",
|
||||||
|
level: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
uploadButton.classList.add("loading");
|
uploadButton.classList.add("loading");
|
||||||
|
|
||||||
// Actual upload logic handled separately (e.g., fetch).
|
// Actual upload logic handled separately (e.g., fetch).
|
||||||
|
NotificationCenter?.show({
|
||||||
|
message: "Upload started…",
|
||||||
|
level: "info",
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
38
static/js/notifications.js
Normal file
38
static/js/notifications.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
(() => {
|
||||||
|
let container;
|
||||||
|
|
||||||
|
function ensureContainer() {
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.className = "toast-container";
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function show({ message, level = "info", timeout = 5000 } = {}) {
|
||||||
|
const root = ensureContainer();
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.className = `toast toast--${level}`;
|
||||||
|
toast.setAttribute("role", "alert");
|
||||||
|
toast.innerHTML = `
|
||||||
|
<span class="toast__icon" aria-hidden="true"></span>
|
||||||
|
<p class="toast__message">${message}</p>
|
||||||
|
<button type="button" class="toast__close" aria-label="Dismiss">×</button>
|
||||||
|
`;
|
||||||
|
root.appendChild(toast);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
toast.classList.add("hidden");
|
||||||
|
setTimeout(() => toast.remove(), 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.querySelector(".toast__close").addEventListener("click", close);
|
||||||
|
|
||||||
|
if (timeout > 0) {
|
||||||
|
setTimeout(close, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.NotificationCenter = { show };
|
||||||
|
})();
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<script src="/static/js/projects.js" defer></script>
|
<script src="/static/js/projects.js" defer></script>
|
||||||
<script src="/static/js/exports.js" defer></script>
|
<script src="/static/js/exports.js" defer></script>
|
||||||
<script src="/static/js/imports.js" defer></script>
|
<script src="/static/js/imports.js" defer></script>
|
||||||
|
<script src="/static/js/notifications.js" defer></script>
|
||||||
<script src="/static/js/theme.js"></script>
|
<script src="/static/js/theme.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn btn-primary">Download</button>
|
<button type="submit" class="btn btn-primary">Download</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="form-error hidden" data-export-error></p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user