feat: enhance export and import workflows with improved error handling and notifications

This commit is contained in:
2025-11-10 18:44:42 +01:00
parent 43b1e53837
commit e2465188c2
6 changed files with 127 additions and 13 deletions

11
static/js/alerts.js Normal file
View 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);
}
});
});
});

View File

@@ -39,20 +39,67 @@ 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']");
method: "POST", if (submitBtn) {
headers: { submitBtn.disabled = true;
"Content-Type": "application/json", submitBtn.classList.add("loading");
}, }
body: JSON.stringify({
format, let response;
include_metadata: formData.get("include_metadata") === "true", try {
filters: null, response = await fetch(submitUrl, {
}), method: "POST",
}); headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
format,
include_metadata: formData.get("include_metadata") === "true",
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",
});
} }
}); });
}); });

View File

@@ -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,
});
}); });
}); });
}); });

View 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 };
})();

View File

@@ -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>

View File

@@ -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>