Version 2.3.0: Speichern mit Ordner und Namen

This commit is contained in:
alientim committed 2026-09-09 22:41:08 +02:00
1 parent e70c6e27c1
commit e7ad00a5e5
8 files changed
+118 -25

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-license" name = "tesm-license"
version = "2.2.1" version = "2.3.0"
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-core" name = "tesm-core"
version = "2.2.1" version = "2.3.0"
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -135,7 +135,7 @@ def create_base_app(
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
#: Anwendungen. #: Anwendungen.
auth_tagline: str = "", auth_tagline: str = "",
app_version: str = "2.2.1", app_version: str = "2.3.0",
built_in_trusted_keys: tuple[str, ...] = (), built_in_trusted_keys: tuple[str, ...] = (),
enable_license: bool = True, enable_license: bool = True,
template_folder: str | None = None, template_folder: str | None = None,
@@ -754,11 +754,19 @@
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
} }
/* pdf.js verlangt einen absolut positionierten, scrollenden Behaelter. */ /* pdf.js verlangt einen absolut positionierten, scrollenden Behaelter --
der liegt deshalb *innen*. Aussen ein gewoehnlicher Flex-Kasten, damit
Zoomleiste und Speicherzeile ihren Platz behalten. Vorher rechnete der
Bereich mit "bottom: var(--sp-10)", also mit genau einer Leiste; die
zweite Zeile lag dann unter dem Dialogrand. */
.viewer__pdf { .viewer__pdf {
position: relative;
flex: 1;
min-height: 0;
}
.viewer__pdfscroll {
position: absolute; position: absolute;
inset: 0; inset: 0;
bottom: var(--sp-10);
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@@ -773,6 +781,26 @@
.viewer__hint { .viewer__hint {
margin-inline-start: auto; margin-inline-start: auto;
} }
.viewer__save {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
border-top: 1px solid var(--border);
background: var(--surface-2, var(--surface-1));
}
.viewer__save .viewer__feld {
flex: 1 1 12rem;
margin: 0;
}
/* Eigener Name: ".field--inline" gibt es schon und steht dort fuer
"waagerecht" -- diese Regel haette sie app-weit umgestellt. */
.viewer__feld {
display: flex;
flex-direction: column;
gap: 2px;
}
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein /* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
unruhig. */ unruhig. */
@@ -902,7 +902,11 @@ function boot() {
einem PDF noch pdf.js nach. Beides erst dann, wenn es gebraucht wird. */ einem PDF noch pdf.js nach. Beides erst dann, wenn es gebraucht wird. */
function initViewer() { function initViewer() {
if (!document.querySelector("[data-viewer]")) return; if (!document.querySelector("[data-viewer]")) return;
import("./viewer.js") // Dieselbe Version wie diese Datei: app.js wird als ".../app.js?v=<Zeit>"
// eingebunden, und nginx liefert /core-assets/ mit "expires 7d". Ohne das
// Anhaengsel bliebe der alte Betrachter nach einem Update im Cache liegen.
const version = new URL(import.meta.url).search;
import(`./viewer.js${version}`)
.then((modul) => modul.initFilePreview()) .then((modul) => modul.initFilePreview())
.catch((fehler) => console.error("Dateibetrachter", fehler)); .catch((fehler) => console.error("Dateibetrachter", fehler));
} }
@@ -146,21 +146,41 @@ function bildBetrachter(buehne, bild) {
let pdfjsGeladen = null; let pdfjsGeladen = null;
/** Die Adressen der pdf.js-Dateien -- alle mit derselben Version.
*
* Uebergeben wird die Adresse von pdf.min.js samt "?v=<Zeit>"; die
* Geschwister liegen daneben. Ohne die Version bliebe nach einem Update die
* alte Fassung im Cache von nginx ("expires 7d") haengen.
*/
function pdfjsAdressen(adresse) {
const url = new URL(adresse, location.href);
const verzeichnis = url.pathname.replace(/[^/]*$/, "");
const mit = (datei) => `${url.origin}${verzeichnis}${datei}${url.search}`;
return {
lib: mit("pdf.min.js"),
komponenten: mit("pdf_viewer.js"),
worker: mit("pdf.worker.min.js"),
stil: mit("pdf_viewer.css"),
schriften: `${url.origin}${verzeichnis}standard_fonts/`,
};
}
/** Laedt pdf.js einmalig nach; das Paket liegt neben dieser Datei. */ /** Laedt pdf.js einmalig nach; das Paket liegt neben dieser Datei. */
async function pdfjsLaden(basis) { async function pdfjsLaden(basis) {
const adressen = pdfjsAdressen(basis);
if (!pdfjsGeladen) { if (!pdfjsGeladen) {
pdfjsGeladen = (async () => { pdfjsGeladen = (async () => {
const lib = await import(`${basis}pdf.min.js`); const lib = await import(adressen.lib);
// Der Komponentenbau von pdf.js sucht die Bibliothek unter diesem Namen. // Der Komponentenbau von pdf.js sucht die Bibliothek unter diesem Namen.
globalThis.pdfjsLib = lib; globalThis.pdfjsLib = lib;
const komponenten = await import(`${basis}pdf_viewer.js`); const komponenten = await import(adressen.komponenten);
// Das Stylesheet von pdf.js zeichnet die Formularfelder. 100 KB gehoeren // Das Stylesheet von pdf.js zeichnet die Formularfelder. 100 KB gehoeren
// nicht in jeden Seitenaufruf -- also erst hier. // nicht in jeden Seitenaufruf -- also erst hier.
const stil = document.createElement("link"); const stil = document.createElement("link");
stil.rel = "stylesheet"; stil.rel = "stylesheet";
stil.href = `${basis}pdf_viewer.css`; stil.href = adressen.stil;
document.head.append(stil); document.head.append(stil);
lib.GlobalWorkerOptions.workerSrc = `${basis}pdf.worker.min.js`; lib.GlobalWorkerOptions.workerSrc = adressen.worker;
return { lib, komponenten }; return { lib, komponenten };
})().catch((fehler) => { })().catch((fehler) => {
pdfjsGeladen = null; pdfjsGeladen = null;
@@ -173,6 +193,7 @@ async function pdfjsLaden(basis) {
/** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */ /** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */
async function pdfBetrachter(behaelter, seiten, url, basis) { async function pdfBetrachter(behaelter, seiten, url, basis) {
const { lib, komponenten } = await pdfjsLaden(basis); const { lib, komponenten } = await pdfjsLaden(basis);
const adressen = pdfjsAdressen(basis);
const eventBus = new komponenten.EventBus(); const eventBus = new komponenten.EventBus();
const linkService = new komponenten.PDFLinkService({ eventBus }); const linkService = new komponenten.PDFLinkService({ eventBus });
const viewer = new komponenten.PDFViewer({ const viewer = new komponenten.PDFViewer({
@@ -190,7 +211,7 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
url, url,
// Ohne die Standardschriften bleibt jede Seite mit Text leer: Helvetica // Ohne die Standardschriften bleibt jede Seite mit Text leer: Helvetica
// und Verwandte liegen als Datei im Paket, nicht im Browser. // und Verwandte liegen als Datei im Paket, nicht im Browser.
standardFontDataUrl: `${basis}standard_fonts/`, standardFontDataUrl: adressen.schriften,
isEvalSupported: false, isEvalSupported: false,
}).promise; }).promise;
viewer.setDocument(dokument); viewer.setDocument(dokument);
@@ -261,11 +282,18 @@ export function initFilePreview(wurzel = document) {
const bildBuehne = stueck("bildbuehne"); const bildBuehne = stueck("bildbuehne");
const bild = stueck("bild"); const bild = stueck("bild");
const pdfBehaelter = stueck("pdf"); const pdfBehaelter = stueck("pdf");
const pdfScroll = stueck("pdfscroll");
const pdfSeiten = stueck("pdfseiten"); const pdfSeiten = stueck("pdfseiten");
const rahmen = stueck("rahmen"); const rahmen = stueck("rahmen");
const leiste = stueck("leiste"); const leiste = stueck("leiste");
const hinweis = stueck("hinweis"); const hinweis = stueck("hinweis");
const speichern = stueck("speichern"); const speichern = stueck("speichern");
const zeile = stueck("speichernzeile");
const ordnerFeld = stueck("ordner");
const nameFeld = stueck("name");
const ersetzenFeld = stueck("ersetzen");
const jetzt = stueck("jetzt");
const abbrechen = stueck("abbrechen");
const basis = rumpf.dataset.viewerPdfjs || ""; const basis = rumpf.dataset.viewerPdfjs || "";
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari), // Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
@@ -309,6 +337,7 @@ export function initFilePreview(wurzel = document) {
} }
pdfSeiten.replaceChildren(); pdfSeiten.replaceChildren();
if (hinweis) hinweis.textContent = ""; if (hinweis) hinweis.textContent = "";
if (zeile) zeile.hidden = true;
aktuell = null; aktuell = null;
}; };
@@ -328,7 +357,7 @@ export function initFilePreview(wurzel = document) {
} }
if (hinweis) hinweis.textContent = "PDF wird geladen ..."; if (hinweis) hinweis.textContent = "PDF wird geladen ...";
try { try {
pdf = await pdfBetrachter(pdfBehaelter, pdfSeiten, quelle, basis); pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis);
if (hinweis) hinweis.textContent = ""; if (hinweis) hinweis.textContent = "";
} catch (fehler) { } catch (fehler) {
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige // Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
@@ -337,8 +366,12 @@ export function initFilePreview(wurzel = document) {
zeigen("text"); zeigen("text");
rahmen.src = quelle; rahmen.src = quelle;
if (hinweis) { if (hinweis) {
// Den Grund mitgeben: auf einem fremden Geraet ist die Meldung des
// Browsers die einzige Spur, die man hat.
const grund = String(fehler?.message || fehler || "").slice(0, 120);
hinweis.textContent = hinweis.textContent =
"Die Formularansicht liess sich nicht laden -- es bleibt bei der einfachen Anzeige."; "Die Formularansicht liess sich nicht laden -- es bleibt bei der einfachen Anzeige." +
(grund ? ` (${grund})` : "");
// Die Leiste traegt den Hinweis. "zeigen('text')" hat sie gerade // Die Leiste traegt den Hinweis. "zeigen('text')" hat sie gerade
// verborgen -- ohne diese Zeile blieb der Fehlschlag unsichtbar, und // verborgen -- ohne diese Zeile blieb der Fehlschlag unsichtbar, und
// es sah aus, als koennte die Vorschau einfach keine Formulare. // es sah aus, als koennte die Vorschau einfach keine Formulare.
@@ -365,33 +398,61 @@ export function initFilePreview(wurzel = document) {
dialog.addEventListener("close", aufraeumen); dialog.addEventListener("close", aufraeumen);
/* Ausgefuelltes Formular zurueck in die Freigabe. */ /* Ausgefuelltes Formular zurueck in die Freigabe -- erst fragen, wohin. */
speichern?.addEventListener("click", async () => {
const vorschlagen = () => {
if (!aktuell) return;
const teile = aktuell.pfad.split("/");
const datei = teile.pop() || "formular.pdf";
ordnerFeld.value = teile.join("/");
// Der Name der Vorlage mit Zusatz -- die Vorlage selbst bleibt so heil,
// auch wenn jemand blind auf Speichern drueckt.
nameFeld.value = datei.replace(/\.pdf$/i, "") + "-ausgefuellt.pdf";
ersetzenFeld.checked = false;
};
speichern?.addEventListener("click", () => {
if (!pdf || !aktuell) return;
vorschlagen();
zeile.hidden = false;
nameFeld.focus();
nameFeld.select();
});
abbrechen?.addEventListener("click", () => {
zeile.hidden = true;
});
jetzt?.addEventListener("click", async () => {
if (!pdf || !aktuell) return; if (!pdf || !aktuell) return;
const ziel = speichern.dataset.url; const ziel = speichern.dataset.url;
speichern.disabled = true; jetzt.disabled = true;
const vorher = speichern.textContent; const vorher = jetzt.textContent;
speichern.textContent = "Speichern ..."; jetzt.textContent = "Speichern ...";
try { try {
const bytes = await pdf.speichern(); const bytes = await pdf.speichern();
const daten = new FormData(); const daten = new FormData();
daten.append("pfad", aktuell.pfad); daten.append("pfad", aktuell.pfad);
daten.append("ordner", ordnerFeld.value.trim());
daten.append("name", nameFeld.value.trim());
if (ersetzenFeld.checked) daten.append("ersetzen", "1");
daten.append( daten.append(
"datei", "datei",
new Blob([bytes], { type: "application/pdf" }), new Blob([bytes], { type: "application/pdf" }),
(aktuell.pfad.split("/").pop() || "formular.pdf") nameFeld.value.trim() || "formular.pdf"
); );
const antwort = await window.tesm.api(ziel, { method: "POST", body: daten }); const antwort = await window.tesm.api(ziel, { method: "POST", body: daten });
window.tesm.showToast(antwort.message || "Gespeichert.", "success"); window.tesm.showToast(antwort.message || "Gespeichert.", "success");
dialog.close(); dialog.close();
window.location.reload(); window.location.reload();
} catch (fehler) { } catch (fehler) {
// Die Meldung fuer den Benutzer, die Einzelheiten fuer die Konsole. // Die Meldung fuer den Benutzer, die Einzelheiten fuer die Konsole. Die
// Zeile bleibt offen: meist ist es ein Name, der schon vergeben ist.
console.error("Formular speichern", fehler); console.error("Formular speichern", fehler);
window.tesm.showToast(fehler.message || "Speichern fehlgeschlagen.", "danger"); window.tesm.showToast(fehler.message || "Speichern fehlgeschlagen.", "danger");
} finally { } finally {
speichern.disabled = false; jetzt.disabled = false;
speichern.textContent = vorher; jetzt.textContent = vorher;
} }
}); });
} }
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-licensing-server" name = "tesm-licensing-server"
version = "2.2.1" version = "2.3.0"
description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten" description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = ["tesm-licensing==2.2.1", "cryptography>=41"] dependencies = ["tesm-licensing==2.3.0", "cryptography>=41"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-licensing" name = "tesm-licensing"
version = "2.2.1" version = "2.3.0"
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver" description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = ["cryptography>=42"] dependencies = ["cryptography>=42"]