App-weite Warnung bei ungespeicherten Aenderungen + Options-UI-Fix + Cache-Busting
1. Generischer Ja/Nein-Dialog bei ungespeicherten Aenderungen (kein Opt-in pro Seite noetig): initUnsavedChangesGuard() erkennt automatisch jedes Formular mit echten Eingabefeldern, das seit dem Laden der Seite veraendert wurde, und fragt beim Klick auf einen Link (z.B. Sidebar- Navigation), ob zuerst gespeichert werden soll -- eigener Dialog (confirmSaveDiscard) statt des bestehenden confirmAction (dort ist "Abbrechen" = Bleiben, hier gibt es bewusst nur die zwei angefragten Optionen: speichern oder verwerfen, beide verlassen die Seite). "Ja, speichern" sendet das Formular ganz normal ab (echter POST) und merkt sich das eigentliche Ziel in sessionStorage, um danach automatisch dorthin weiterzuleiten. Nachtrag noch in dieser Session gefixt: ein normales Speichern ueber den Seiten-eigenen Button loeste zusaetzlich die native Browser-Warnung (beforeunload) aus, obwohl die Aenderung bereits gespeichert wurde -- navigatingAway-Flag wird jetzt bei jedem Submit eines beobachteten Formulars (eigener Button ODER eigener Ja/Nein-Dialog) gesetzt, die generische Browser-Warnung erscheint dadurch nur noch bei tatsaechlich ungefragter Navigation (Tab schliessen, Reload, Adresszeile). Live per Playwright in drei Szenarien verifiziert (direktes Speichern, Ja via eigenem Dialog, Nein via eigenem Dialog) -- kein Browser-Popup in den ersten beiden Faellen, korrektes Redirect zum urspruenglichen Ziel nach "Ja". 2. DHCP-Options-Dropdown (Reservierungs-Dialog) verschiebt ein neu ausgewaehltes Options-Feld jetzt direkt unter das Dropdown (in einen .poe-added-options-Container) statt es nur an seiner urspruenglichen Stelle in der bis zu 57 Eintraege langen Liste einzublenden -- lag dort oft ausserhalb des sichtbaren Modal-Ausschnitts und wirkte, als waere nichts passiert. Bereits gesetzte Overrides stehen von Anfang an im selben Container, weitere haengen sich beim Auswaehlen unten an. 3. Cache-Busting fuer app.js/style.css (asset_url()-Helper, haengt die Aenderungszeit der Datei als ?v=... an) -- ein im Browser gecachtes, veraltetes app.js konnte nach einem Update sonst den Eindruck erwecken, eine neue Funktion wuerde "nicht funktionieren", obwohl der Code auf dem Server laengst aktuell war. Alle drei live auf der Test-VM deployt und per Playwright verifiziert, keine Fehler im journalctl-Log. (Testartefakt aufgeraeumt: der echte Host-Hostname wurde durch die Playwright-Tests versehentlich auf "directsave-test"/"navsave-test" geaendert und wieder auf POETEST zurueckgesetzt.)
This commit is contained in:
@@ -154,6 +154,136 @@
|
||||
openModal("confirm-modal");
|
||||
};
|
||||
|
||||
/* ---------------- Ja/Nein-Dialog für ungespeicherte Änderungen ---------------- */
|
||||
/* Eigener Dialog statt confirmAction (dort ist "Abbrechen" = beim Bleiben,
|
||||
hier gibt es bewusst nur die zwei angefragten Optionen: speichern oder
|
||||
verwerfen — beide verlassen die Seite, nur "Abbrechen"/Escape/Backdrop
|
||||
bricht das Verlassen selbst ab und lässt die Seite unverändert offen. */
|
||||
function ensureSaveDiscardModal() {
|
||||
if (document.getElementById("save-discard-modal")) return;
|
||||
const html = `
|
||||
<div class="modal-overlay" id="save-discard-modal">
|
||||
<div class="modal" style="max-width:420px;">
|
||||
<div class="modal-header">
|
||||
<h3>Ungespeicherte Änderungen</h3>
|
||||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-dim" id="save-discard-message"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="save-discard-no">Nein, verwerfen</button>
|
||||
<button type="button" class="btn btn-primary" id="save-discard-yes">Ja, speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.insertAdjacentHTML("beforeend", html);
|
||||
document.querySelectorAll("#save-discard-modal [data-close-modal]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => closeModal(btn));
|
||||
});
|
||||
document.getElementById("save-discard-modal").addEventListener("click", (e) => {
|
||||
if (e.target.id === "save-discard-modal") closeModal(e.target);
|
||||
});
|
||||
}
|
||||
|
||||
window.confirmSaveDiscard = function (message, onSave, onDiscard) {
|
||||
ensureSaveDiscardModal();
|
||||
document.getElementById("save-discard-message").innerText = message;
|
||||
const yesBtn = document.getElementById("save-discard-yes");
|
||||
const noBtn = document.getElementById("save-discard-no");
|
||||
const freshYes = yesBtn.cloneNode(true);
|
||||
yesBtn.parentNode.replaceChild(freshYes, yesBtn);
|
||||
const freshNo = noBtn.cloneNode(true);
|
||||
noBtn.parentNode.replaceChild(freshNo, noBtn);
|
||||
freshYes.addEventListener("click", () => { closeModal(freshYes); onSave(); });
|
||||
freshNo.addEventListener("click", () => { closeModal(freshNo); onDiscard(); });
|
||||
openModal("save-discard-modal");
|
||||
};
|
||||
|
||||
/* ---------------- Warnung bei ungespeicherten Änderungen ---------------- */
|
||||
/* Erkennt generisch auf JEDER Seite, ob ein Formular mit echten
|
||||
Eingabefeldern (nicht nur versteckten Aktions-Feldern wie bei Löschen/
|
||||
Toggle-Buttons) seit dem Laden der Seite verändert wurde, und fragt vor
|
||||
dem Verlassen (Klick auf einen Link, z.B. in der Navigation) per Ja/Nein,
|
||||
ob zuerst gespeichert werden soll — statt Änderungen stillschweigend zu
|
||||
verwerfen. "Ja" sendet das Formular ganz normal ab (echter POST, kein
|
||||
AJAX-Nachbau nötig) und merkt sich das eigentliche Ziel in
|
||||
sessionStorage, um nach dem Speichern automatisch dorthin
|
||||
weiterzuleiten, statt dass der Link ein zweites Mal angeklickt werden
|
||||
muss. Für Browser-eigene Navigation (Tab schließen, Reload, Adresszeile)
|
||||
gibt es zusätzlich beforeunload — dort erlaubt der Browser aus
|
||||
Sicherheitsgründen aber nur eine generische Warnung, kein eigenes
|
||||
Ja/Nein/Speichern-Dialogfeld. */
|
||||
const PENDING_NAV_KEY = "poe-pending-nav-after-save";
|
||||
|
||||
function serializeForm(form) {
|
||||
return new URLSearchParams(new FormData(form)).toString();
|
||||
}
|
||||
|
||||
function initUnsavedChangesGuard() {
|
||||
const pendingNav = sessionStorage.getItem(PENDING_NAV_KEY);
|
||||
if (pendingNav) {
|
||||
sessionStorage.removeItem(PENDING_NAV_KEY);
|
||||
window.location.href = pendingNav;
|
||||
return;
|
||||
}
|
||||
|
||||
const trackable = [];
|
||||
document.querySelectorAll("form[method='post' i]").forEach((form) => {
|
||||
if (form.hasAttribute("data-no-unsaved-guard")) return;
|
||||
const fields = form.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]), select, textarea");
|
||||
if (!fields.length) return; // reine Aktions-Formulare (Löschen/Toggle) ohne Eingabefeld
|
||||
trackable.push({ form, initial: serializeForm(form) });
|
||||
});
|
||||
if (!trackable.length) return;
|
||||
|
||||
// Verhindert die native Browser-Warnung (beforeunload), wenn die Seite
|
||||
// ohnehin schon bewusst verlassen wird — entweder weil eines der
|
||||
// beobachteten Formulare ganz normal abgeschickt wurde (eigener
|
||||
// "Speichern"/"Anlegen"-Button auf der Seite) oder weil unser eigener
|
||||
// Ja/Nein-Dialog die Navigation ausgelöst hat. Ohne das würde beim
|
||||
// normalen Speichern zusätzlich zum eigentlichen Erfolg immer noch die
|
||||
// generische Browser-Meldung aufpoppen — die soll ausschließlich dann
|
||||
// erscheinen, wenn tatsächlich UNGEFRAGT navigiert wird (Tab schließen,
|
||||
// Reload, Adresszeile), nicht bei einem bewussten Speichern.
|
||||
let navigatingAway = false;
|
||||
trackable.forEach(({ form }) => {
|
||||
form.addEventListener("submit", () => { navigatingAway = true; });
|
||||
});
|
||||
|
||||
function dirtyForm() {
|
||||
return trackable.find(({ form, initial }) => serializeForm(form) !== initial) || null;
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const link = e.target.closest("a[href]");
|
||||
if (!link || link.target === "_blank" || link.href.startsWith("javascript:") || link.hasAttribute("data-no-unsaved-guard")) return;
|
||||
const dirty = dirtyForm();
|
||||
if (!dirty) return;
|
||||
e.preventDefault();
|
||||
const href = link.href;
|
||||
window.confirmSaveDiscard(
|
||||
"Es gibt ungespeicherte Änderungen auf dieser Seite. Vor dem Verlassen speichern?",
|
||||
() => {
|
||||
navigatingAway = true;
|
||||
sessionStorage.setItem(PENDING_NAV_KEY, href);
|
||||
dirty.form.requestSubmit ? dirty.form.requestSubmit() : dirty.form.submit();
|
||||
},
|
||||
() => {
|
||||
navigatingAway = true;
|
||||
window.location.href = href;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
if (!navigatingAway && dirtyForm()) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Intercept forms/buttons marked with data-confirm="message" */
|
||||
function initConfirmables() {
|
||||
document.querySelectorAll("form[data-confirm]").forEach((form) => {
|
||||
@@ -205,17 +335,21 @@
|
||||
Feld (siehe settings_dhcp.html, "+ Option hinzufügen"-Auswahl bei
|
||||
Reservierungen) — so sind nur tatsächlich genutzte Options-Felder
|
||||
sichtbar, statt jede einzeln immer als leeres Eingabefeld anzuzeigen.
|
||||
Scrollt das neue Feld zusätzlich in den sichtbaren Bereich und fokussiert
|
||||
es — bei bis zu ~60 möglichen Optionen kann das neu eingeblendete Feld
|
||||
sonst weit unterhalb des sichtbaren Modal-Ausschnitts liegen und wirkt
|
||||
dann so, als wäre gar nichts passiert. */
|
||||
Verschiebt das Feld dafür direkt unter das Dropdown (in den Container
|
||||
mit der Klasse "poe-added-options", der im Markup direkt danach steht)
|
||||
statt es nur an seiner ursprünglichen Stelle einzublenden — bei bis zu
|
||||
~60 möglichen Optionen läge diese sonst weit unterhalb des sichtbaren
|
||||
Modal-Ausschnitts und es wirkte so, als wäre gar nichts passiert. Jede
|
||||
weitere Auswahl hängt ihr Feld unten an, sodass alle bereits
|
||||
hinzugefügten Options sichtbar aufgereiht bleiben. */
|
||||
window.poeShowOptionField = function (select) {
|
||||
const id = select.value;
|
||||
if (id) {
|
||||
const field = document.getElementById(id);
|
||||
if (field) {
|
||||
const container = select.nextElementSibling;
|
||||
if (field && container && container.classList.contains("poe-added-options")) {
|
||||
container.appendChild(field);
|
||||
field.style.display = "";
|
||||
field.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
const input = field.querySelector("input");
|
||||
if (input) input.focus();
|
||||
}
|
||||
@@ -407,6 +541,7 @@
|
||||
initCheckTimer();
|
||||
initRunCheckNow();
|
||||
initLeaseCountdowns();
|
||||
initUnsavedChangesGuard();
|
||||
initSortableTables();
|
||||
document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user