From 81964c76d32b4596b0b8144c27415caf5431c324 Mon Sep 17 00:00:00 2001 From: sylyx Date: Tue, 16 Jun 2026 15:01:37 +0200 Subject: [PATCH] Mapping: feste Werte ({const}) + Teilen via JSON-Copy/Paste und Pull von raw-JSON-URL (mappingUrl) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prefillFor löst Token-String ODER {const:...} auf; Options-Editor mit 'Fester Wert'-Option + Konstanten-Eingabe. - Feld-Zuordnung: JSON exportieren/anwenden/kopieren, von URL laden (Gitea/GitHub raw). - Fix: zwei Statustexte mit ASCII-Quote in " gehärtet. Co-Authored-By: Claude Opus 4.8 --- dialog/dialog.js | 13 ++++-- lib/store.js | 5 ++- options/options.html | 38 +++++++++++++++++ options/options.js | 97 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 142 insertions(+), 11 deletions(-) diff --git a/dialog/dialog.js b/dialog/dialog.js index f831aba..2c1470e 100644 --- a/dialog/dialog.js +++ b/dialog/dialog.js @@ -39,10 +39,17 @@ function effectiveFieldMap() { return m && Object.keys(m).length ? m : DEFAULT_FIELD_MAP; } -// Vorbefüllwert für ein DocuWare-Feld aus den Mail-Metadaten (oder undefined). +// Vorbefüllwert für ein DocuWare-Feld (oder undefined). Das Mapping kann je Feld +// entweder einen Mail-Token (String) ODER einen festen Wert ({ const: "NEU" }) tragen. function prefillFor(fieldName, meta) { - const token = effectiveFieldMap()[fieldName]; - const fn = token && TOKEN_FN[token]; + const m = effectiveFieldMap()[fieldName]; + if (!m) return undefined; + if (typeof m === "object") { + if (m.const !== undefined && m.const !== null && m.const !== "") return m.const; + if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta); + return undefined; + } + const fn = TOKEN_FN[m]; return fn ? fn(meta) : undefined; } diff --git a/lib/store.js b/lib/store.js index 6c6cfab..b175436 100644 --- a/lib/store.js +++ b/lib/store.js @@ -12,7 +12,10 @@ const DEFAULTS = { mailFormat: "eml", // E-Mail wird als "eml" ODER "pdf" abgelegt (nie beides) storeAttachments: true, tagOnSuccess: true, - fieldMapping: {}, // DocuWare-Feldname -> Mail-Attribut (Token). Leer = DEFAULT_FIELD_MAP. + // DocuWare-Feldname -> Wert: entweder ein Mail-Token ("subject") ODER ein fester + // Wert ({ const: "NEU" }). Leer = DEFAULT_FIELD_MAP. + fieldMapping: {}, + mappingUrl: "", // optional: raw-JSON-URL (Gitea/GitHub) zum Nachladen des Mappings. }; // Mögliche Mail-Attribute für die Vorbefüllung (Token + Anzeigename). diff --git a/options/options.html b/options/options.html index d0ea903..1509bdc 100644 --- a/options/options.html +++ b/options/options.html @@ -110,6 +110,22 @@ } .icon-btn:hover { color: var(--err); border-color: var(--err); } + /* Teilen / JSON */ + details.share { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 10px; } + details.share summary { + cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent); + list-style: none; user-select: none; padding: 2px 0; + } + details.share summary::-webkit-details-marker { display: none; } + details.share summary::before { content: "▸ "; color: var(--muted); } + details.share[open] summary::before { content: "▾ "; } + textarea { + width: 100%; padding: 10px 11px; color: var(--text); background: var(--surface-2); + border: 1px solid var(--border-strong); border-radius: 8px; box-sizing: border-box; + font: 12px/1.45 ui-monospace, "Cascadia Code", Consolas, monospace; resize: vertical; outline: none; + } + textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); } + /* Buttons */ button { font: inherit; cursor: pointer; border-radius: 8px; } button.primary { @@ -206,6 +222,28 @@ +
Als Mail-Attribut „Fester Wert" + wählen, um einen festen Text (z. B. NEU) ins Feld zu schreiben.
+ + diff --git a/options/options.js b/options/options.js index 25e4f8e..1215d1c 100644 --- a/options/options.js +++ b/options/options.js @@ -17,6 +17,7 @@ async function load() { FIELDS.forEach((f) => ($(f).value = s[f] || "")); CHECKS.forEach((c) => ($(c).checked = !!s[c])); setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml"); + $("mappingUrl").value = s.mappingUrl || ""; loadMapping(s.fieldMapping && Object.keys(s.fieldMapping).length ? s.fieldMapping : DEFAULT_FIELD_MAP); // Schrankliste, falls vorher schon getestet (im Storage gecached) const cached = (await browser.storage.local.get("cabinets")).cabinets || []; @@ -81,7 +82,20 @@ async function ensureToken() { // --- Feld-Mapping-Editor --------------------------------------------------- -function addMapRow(field = "", token = "") { +const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert" + +// Zerlegt einen Mapping-Wert (Token-String ODER { const } / { token }) in den UI-Zustand. +function mapValueState(value) { + if (value && typeof value === "object") { + if (value.const != null) return { sel: CONST_OPT, konst: String(value.const) }; + if (value.token) return { sel: value.token, konst: "" }; + } + if (typeof value === "string" && value) return { sel: value, konst: "" }; + return { sel: "", konst: "" }; +} + +function addMapRow(field = "", value = "") { + const st = mapValueState(value); const tr = document.createElement("tr"); const inp = document.createElement("input"); @@ -90,16 +104,31 @@ function addMapRow(field = "", token = "") { inp.setAttribute("list", "fieldNames"); // Vorschläge aus gewähltem Archiv const tdF = document.createElement("td"); tdF.appendChild(inp); - const sel = document.createElement("select"); sel.className = "map-token"; + const sel = document.createElement("select"); sel.className = "map-source"; const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —"; sel.appendChild(none); MAIL_TOKENS.forEach((t) => { const o = document.createElement("option"); o.value = t.key; o.textContent = t.label; - if (t.key === token) o.selected = true; + if (t.key === st.sel) o.selected = true; sel.appendChild(o); }); - const tdT = document.createElement("td"); tdT.appendChild(sel); + const co = document.createElement("option"); + co.value = CONST_OPT; co.textContent = "★ Fester Wert"; + if (st.sel === CONST_OPT) co.selected = true; + sel.appendChild(co); + + const konst = document.createElement("input"); + konst.type = "text"; konst.className = "map-const"; konst.placeholder = "z. B. NEU"; + konst.value = st.konst; konst.style.marginTop = "6px"; + konst.style.display = st.sel === CONST_OPT ? "" : "none"; + + sel.addEventListener("change", () => { + konst.style.display = sel.value === CONST_OPT ? "" : "none"; + if (sel.value === CONST_OPT) konst.focus(); + }); + + const tdT = document.createElement("td"); tdT.appendChild(sel); tdT.appendChild(konst); const del = document.createElement("button"); del.type = "button"; del.className = "icon-btn"; del.textContent = "✕"; del.title = "Zeile entfernen"; @@ -112,15 +141,21 @@ function addMapRow(field = "", token = "") { function loadMapping(map) { $("mapBody").innerHTML = ""; - Object.entries(map || {}).forEach(([f, t]) => addMapRow(f, t)); + Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v)); } function collectMapping() { const map = {}; document.querySelectorAll("#mapBody tr").forEach((tr) => { const f = tr.querySelector(".map-field").value.trim(); - const t = tr.querySelector(".map-token").value; - if (f && t) map[f] = t; // leere Feldnamen/Token ignorieren + if (!f) return; // ohne Feldname ignorieren + const sel = tr.querySelector(".map-source").value; + if (sel === CONST_OPT) { + const c = tr.querySelector(".map-const").value; + if (c !== "") map[f] = { const: c }; // fester Wert + } else if (sel) { + map[f] = sel; // Mail-Token (als String, abwärtskompatibel) + } }); return map; } @@ -132,6 +167,7 @@ function collect() { partial.defaultCabinetId = $("defaultCabinet").value; partial.mailFormat = getMailFormat(); partial.fieldMapping = collectMapping(); + partial.mappingUrl = $("mappingUrl").value.trim(); return partial; } @@ -162,6 +198,53 @@ $("test").addEventListener("click", async () => { $("mapAdd").addEventListener("click", () => addMapRow()); $("mapReset").addEventListener("click", () => loadMapping(DEFAULT_FIELD_MAP)); +// --- Mapping teilen: JSON anzeigen / kopieren / anwenden / von URL laden ---- + +$("mappingExport").addEventListener("click", () => { + $("mappingJson").value = JSON.stringify(collectMapping(), null, 2); + setStatus("Aktuelles Mapping als JSON angezeigt.", "ok"); +}); + +$("mappingCopy").addEventListener("click", async () => { + let txt = $("mappingJson").value.trim(); + if (!txt) { txt = JSON.stringify(collectMapping(), null, 2); $("mappingJson").value = txt; } + try { + await navigator.clipboard.writeText(txt); + setStatus("In Zwischenablage kopiert.", "ok"); + } catch (_) { + $("mappingJson").select(); document.execCommand("copy"); + setStatus("In Zwischenablage kopiert.", "ok"); + } +}); + +$("mappingApply").addEventListener("click", () => { + try { + const obj = JSON.parse($("mappingJson").value); + if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("kein JSON-Objekt"); + loadMapping(obj); + setStatus("JSON ins Mapping übernommen – mit „Speichern“ sichern.", "ok"); + } catch (e) { + setStatus(`Ungültiges JSON: ${e.message}`, "err"); + } +}); + +$("mappingPull").addEventListener("click", async () => { + const url = $("mappingUrl").value.trim(); + if (!url) { setStatus("Bitte eine URL eingeben.", "err"); return; } + setStatus("Lade Mapping …"); + try { + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const obj = await res.json(); + if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("kein JSON-Objekt"); + loadMapping(obj); + await Settings.set({ mappingUrl: url }); // URL gleich merken + setStatus("Mapping von URL geladen – mit „Speichern“ übernehmen.", "ok"); + } catch (e) { + setStatus(`Laden fehlgeschlagen: ${e.message}`, "err"); + } +}); + // Archiv wählen -> dessen Indexfelder als Vorschläge (datalist) für alle Feldnamen. $("mapCabinet").addEventListener("change", async () => { const id = $("mapCabinet").value;