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]
|
[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 = [
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
|
|||||||
Reference in new issue
Block a user