Version 2.3.0: Speichern mit Ordner und Namen
This commit is contained in:
1 parent
e70c6e27c1
commit
e7ad00a5e5
8 files changed
+118
-25
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-license"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
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"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -135,7 +135,7 @@ def create_base_app(
|
||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||
#: Anwendungen.
|
||||
auth_tagline: str = "",
|
||||
app_version: str = "2.2.1",
|
||||
app_version: str = "2.3.0",
|
||||
built_in_trusted_keys: tuple[str, ...] = (),
|
||||
enable_license: bool = True,
|
||||
template_folder: str | None = None,
|
||||
|
||||
@@ -754,11 +754,19 @@
|
||||
user-select: 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 {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.viewer__pdfscroll {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
bottom: var(--sp-10);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@@ -773,6 +781,26 @@
|
||||
.viewer__hint {
|
||||
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
|
||||
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
||||
unruhig. */
|
||||
|
||||
@@ -902,7 +902,11 @@ function boot() {
|
||||
einem PDF noch pdf.js nach. Beides erst dann, wenn es gebraucht wird. */
|
||||
function initViewer() {
|
||||
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())
|
||||
.catch((fehler) => console.error("Dateibetrachter", fehler));
|
||||
}
|
||||
|
||||
@@ -146,21 +146,41 @@ function bildBetrachter(buehne, bild) {
|
||||
|
||||
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. */
|
||||
async function pdfjsLaden(basis) {
|
||||
const adressen = pdfjsAdressen(basis);
|
||||
if (!pdfjsGeladen) {
|
||||
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.
|
||||
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
|
||||
// nicht in jeden Seitenaufruf -- also erst hier.
|
||||
const stil = document.createElement("link");
|
||||
stil.rel = "stylesheet";
|
||||
stil.href = `${basis}pdf_viewer.css`;
|
||||
stil.href = adressen.stil;
|
||||
document.head.append(stil);
|
||||
lib.GlobalWorkerOptions.workerSrc = `${basis}pdf.worker.min.js`;
|
||||
lib.GlobalWorkerOptions.workerSrc = adressen.worker;
|
||||
return { lib, komponenten };
|
||||
})().catch((fehler) => {
|
||||
pdfjsGeladen = null;
|
||||
@@ -173,6 +193,7 @@ async function pdfjsLaden(basis) {
|
||||
/** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */
|
||||
async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
const { lib, komponenten } = await pdfjsLaden(basis);
|
||||
const adressen = pdfjsAdressen(basis);
|
||||
const eventBus = new komponenten.EventBus();
|
||||
const linkService = new komponenten.PDFLinkService({ eventBus });
|
||||
const viewer = new komponenten.PDFViewer({
|
||||
@@ -190,7 +211,7 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
url,
|
||||
// Ohne die Standardschriften bleibt jede Seite mit Text leer: Helvetica
|
||||
// und Verwandte liegen als Datei im Paket, nicht im Browser.
|
||||
standardFontDataUrl: `${basis}standard_fonts/`,
|
||||
standardFontDataUrl: adressen.schriften,
|
||||
isEvalSupported: false,
|
||||
}).promise;
|
||||
viewer.setDocument(dokument);
|
||||
@@ -261,11 +282,18 @@ export function initFilePreview(wurzel = document) {
|
||||
const bildBuehne = stueck("bildbuehne");
|
||||
const bild = stueck("bild");
|
||||
const pdfBehaelter = stueck("pdf");
|
||||
const pdfScroll = stueck("pdfscroll");
|
||||
const pdfSeiten = stueck("pdfseiten");
|
||||
const rahmen = stueck("rahmen");
|
||||
const leiste = stueck("leiste");
|
||||
const hinweis = stueck("hinweis");
|
||||
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 || "";
|
||||
|
||||
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
||||
@@ -309,6 +337,7 @@ export function initFilePreview(wurzel = document) {
|
||||
}
|
||||
pdfSeiten.replaceChildren();
|
||||
if (hinweis) hinweis.textContent = "";
|
||||
if (zeile) zeile.hidden = true;
|
||||
aktuell = null;
|
||||
};
|
||||
|
||||
@@ -328,7 +357,7 @@ export function initFilePreview(wurzel = document) {
|
||||
}
|
||||
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
||||
try {
|
||||
pdf = await pdfBetrachter(pdfBehaelter, pdfSeiten, quelle, basis);
|
||||
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis);
|
||||
if (hinweis) hinweis.textContent = "";
|
||||
} catch (fehler) {
|
||||
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
|
||||
@@ -337,8 +366,12 @@ export function initFilePreview(wurzel = document) {
|
||||
zeigen("text");
|
||||
rahmen.src = quelle;
|
||||
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 =
|
||||
"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
|
||||
// verborgen -- ohne diese Zeile blieb der Fehlschlag unsichtbar, und
|
||||
// es sah aus, als koennte die Vorschau einfach keine Formulare.
|
||||
@@ -365,33 +398,61 @@ export function initFilePreview(wurzel = document) {
|
||||
|
||||
dialog.addEventListener("close", aufraeumen);
|
||||
|
||||
/* Ausgefuelltes Formular zurueck in die Freigabe. */
|
||||
speichern?.addEventListener("click", async () => {
|
||||
/* Ausgefuelltes Formular zurueck in die Freigabe -- erst fragen, wohin. */
|
||||
|
||||
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;
|
||||
const ziel = speichern.dataset.url;
|
||||
speichern.disabled = true;
|
||||
const vorher = speichern.textContent;
|
||||
speichern.textContent = "Speichern ...";
|
||||
jetzt.disabled = true;
|
||||
const vorher = jetzt.textContent;
|
||||
jetzt.textContent = "Speichern ...";
|
||||
try {
|
||||
const bytes = await pdf.speichern();
|
||||
const daten = new FormData();
|
||||
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(
|
||||
"datei",
|
||||
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 });
|
||||
window.tesm.showToast(antwort.message || "Gespeichert.", "success");
|
||||
dialog.close();
|
||||
window.location.reload();
|
||||
} 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);
|
||||
window.tesm.showToast(fehler.message || "Speichern fehlgeschlagen.", "danger");
|
||||
} finally {
|
||||
speichern.disabled = false;
|
||||
speichern.textContent = vorher;
|
||||
jetzt.disabled = false;
|
||||
jetzt.textContent = vorher;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing-server"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["tesm-licensing==2.2.1", "cryptography>=41"]
|
||||
dependencies = ["tesm-licensing==2.3.0", "cryptography>=41"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["cryptography>=42"]
|
||||
|
||||
Reference in new issue
Block a user