+Datenexport

This commit is contained in:
Simon Martens
2026-01-28 17:26:04 +01:00
parent de37145471
commit b0a57884bf
19 changed files with 3729 additions and 1931 deletions

View File

@@ -0,0 +1,198 @@
export class ExportManager extends HTMLElement {
constructor() {
super();
this.listUrl = "";
this.runUrl = "";
this.deleteUrl = "";
this.csrf = "";
this.list = null;
this.status = null;
this.pollTimer = null;
this.pollIntervalMs = 2500;
}
connectedCallback() {
this.listUrl = this.dataset.listUrl || "";
this.runUrl = this.dataset.runUrl || "";
this.deleteUrl = this.dataset.deleteUrl || "";
this.csrf = this.dataset.csrf || "";
this.list = this.querySelector("[data-role='export-list']");
this.status = this.querySelector("[data-role='status']");
this.addEventListener("click", (event) => this.handleAction(event));
this.refreshList();
}
disconnectedCallback() {
this.stopPolling();
}
setStatus(message, isError) {
if (!this.status) return;
this.status.textContent = message || "";
this.status.classList.remove("text-red-600", "text-green-600");
if (isError) {
this.status.classList.add("text-red-600");
} else if (message) {
this.status.classList.add("text-green-600");
}
}
async handleRun(event, exportType) {
event.preventDefault();
if (!this.runUrl) return;
this.setStatus("Export wird gestartet...");
const button = event.target.closest("[data-role='run-export']");
if (button) button.disabled = true;
const payload = new URLSearchParams();
payload.set("csrf_token", this.getCsrfToken());
payload.set("export_type", exportType || "data");
try {
const response = await fetch(this.runUrl, {
method: "POST",
body: payload,
credentials: "same-origin",
});
if (!response.ok) {
const message = await this.extractError(response);
this.setStatus(message || "Export konnte nicht gestartet werden.", true);
return;
}
const json = await this.safeJson(response);
if (json && json.error) {
this.setStatus(json.error, true);
return;
}
this.setStatus("Export läuft im Hintergrund.");
await this.refreshList();
this.startPolling();
} catch (err) {
this.setStatus("Export konnte nicht gestartet werden.", true);
} finally {
if (button) button.disabled = false;
}
}
async handleAction(event) {
const runTarget = event.target.closest("[data-role='run-export']");
if (runTarget) {
const exportType = runTarget.getAttribute("data-export-type") || "data";
await this.handleRun(event, exportType);
return;
}
const target = event.target.closest("[data-action]");
if (!target) return;
const action = target.getAttribute("data-action");
if (action === "delete") {
const id = target.getAttribute("data-id");
if (!id || !this.deleteUrl) return;
const confirmed = confirm("Soll der Export wirklich gelöscht werden?");
if (!confirmed) return;
await this.deleteExport(id);
}
}
async deleteExport(id) {
const payload = new URLSearchParams();
payload.set("csrf_token", this.getCsrfToken());
try {
const response = await fetch(`${this.deleteUrl}${id}`, {
method: "POST",
body: payload,
credentials: "same-origin",
});
if (!response.ok) {
const message = await this.extractError(response);
this.setStatus(message || "Export konnte nicht gelöscht werden.", true);
return;
}
const json = await this.safeJson(response);
if (json && json.error) {
this.setStatus(json.error, true);
return;
}
this.setStatus("Export gelöscht.");
await this.refreshList();
} catch {
this.setStatus("Export konnte nicht gelöscht werden.", true);
}
}
async refreshList() {
if (!this.list || !this.listUrl) return;
try {
const response = await fetch(this.listUrl, { credentials: "same-origin" });
if (!response.ok) {
return;
}
const html = await response.text();
this.list.innerHTML = html;
this.syncPollingState();
} catch {
// ignore refresh errors
}
}
syncPollingState() {
if (!this.list) return;
const active = this.list.querySelector("[data-export-status='running'], [data-export-status='queued']");
if (active) {
this.startPolling();
} else {
this.stopPolling();
}
}
startPolling() {
if (this.pollTimer) return;
this.pollTimer = window.setInterval(() => {
this.refreshList();
}, this.pollIntervalMs);
}
stopPolling() {
if (!this.pollTimer) return;
window.clearInterval(this.pollTimer);
this.pollTimer = null;
}
async safeJson(response) {
try {
return await response.json();
} catch {
return null;
}
}
async extractError(response) {
try {
const json = await response.json();
if (json && json.error) {
return json.error;
}
} catch {
// ignore
}
try {
return await response.text();
} catch {
return "";
}
}
getCsrfToken() {
if (this.csrf) return this.csrf;
const fallback = document.querySelector("input[name='csrf_token']");
if (fallback && fallback.value) {
this.csrf = fallback.value;
}
return this.csrf;
}
}

View File

@@ -337,6 +337,15 @@ export class FabMenu extends HTMLElement {
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Administration
</div>
<div class="grid grid-cols-[1fr_auto] group">
<a href="/redaktion/exports/" class="flex items-center px-4 py-2 group-hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-download-2-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Datenexport</span>
</a>
<a href="/redaktion/exports/" target="_blank" class="flex items-center justify-center px-3 py-2 group-hover:bg-gray-100 text-gray-700 hover:text-slate-900 transition-colors no-underline text-sm" title="In neuem Tab öffnen">
<i class="ri-external-link-line text-base"></i>
</a>
</div>
<div class="grid grid-cols-[1fr_auto] group">
<a href="/user/management/access/User?redirectTo=${redirectPath}" class="flex items-center px-4 py-2 group-hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-group-3-line text-base text-gray-700 mr-2.5"></i>

View File

@@ -30,6 +30,7 @@ import { FabMenu } from "./fab-menu.js";
import { DuplicateWarningChecker } from "./duplicate-warning.js";
import { ContentImages } from "./content-images.js";
import { LookupField } from "./lookup-field.js";
import { ExportManager } from "./export-manager.js";
const FILTER_LIST_ELEMENT = "filter-list";
const FAB_MENU_ELEMENT = "fab-menu";
@@ -53,6 +54,7 @@ const EDIT_PAGE_ELEMENT = "edit-page";
const DUPLICATE_WARNING_ELEMENT = "duplicate-warning-checker";
const CONTENT_IMAGES_ELEMENT = "content-images";
const LOOKUP_FIELD_ELEMENT = "lookup-field";
const EXPORT_MANAGER_ELEMENT = "export-manager";
window.lookupSeriesValue = ({ item }) => item?.id || "";
window.lookupSeriesLink = ({ item }) => (item?.musenalm_id ? `/reihe/${item.musenalm_id}` : "");
@@ -81,6 +83,7 @@ customElements.define(FAB_MENU_ELEMENT, FabMenu);
customElements.define(DUPLICATE_WARNING_ELEMENT, DuplicateWarningChecker);
customElements.define(CONTENT_IMAGES_ELEMENT, ContentImages);
customElements.define(LOOKUP_FIELD_ELEMENT, LookupField);
customElements.define(EXPORT_MANAGER_ELEMENT, ExportManager);
function PathPlusQuery() {
const path = window.location.pathname;