Compare commits

..

No commits in common. "main" and "v0.10.4" have entirely different histories.

9 changed files with 118 additions and 395 deletions

View File

@ -1,127 +1,97 @@
/* DocuWare-naher Dark-Style identische Palette wie die Optionsseite. */
:root {
--bg: #11151b;
--surface: #1a1f27;
--surface-2: #222a34;
--border: #2b3340;
--border-strong: #38424f;
--text: #e8edf2;
--muted: #8c97a4;
--accent: #2aa4df;
--accent-hover: #3cb6f0;
--accent-weak: rgba(42,164,223,.14);
--ok: #46c98a;
--err: #ec6b75;
color-scheme: dark; /* native Datepicker/Scrollbars/Checkboxen im Dark-Mode */
}
* { box-sizing: border-box; }
body {
font: 13px/1.45 "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
margin: 0; color: var(--text); background: var(--bg);
font: 13px/1.45 "Segoe UI", system-ui, sans-serif;
margin: 0; color: #1a1a1a; background: #fff;
display: flex; flex-direction: column; height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* Titel-Leiste */
/* Titel-Leiste wie im Connect-to-Outlook-Dialog */
.titlebar {
background: #0f1318; color: var(--text);
padding: 9px 16px; font-weight: 600; font-size: 13px; letter-spacing: .2px;
border-bottom: 1px solid var(--border);
box-shadow: 0 1px 0 rgba(42,164,223,.35);
background: #2b3a44; color: #fff;
padding: 8px 14px; font-weight: 600; font-size: 13px;
flex-shrink: 0;
}
header { padding: 14px 18px 8px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
header h1 { font-size: 16px; font-weight: 650; margin: 0 0 10px; }
header { padding: 12px 18px 6px; border-bottom: 1px solid #e5e7eb; flex-shrink: 0; }
header h1 { font-size: 16px; font-weight: 600; margin: 0 0 10px; }
.toolbar { display: flex; align-items: center; gap: 14px; }
.toolbar .spacer { flex: 1; }
button { font: inherit; cursor: pointer; }
button.link {
background: none; border: 0; color: var(--muted);
padding: 4px 2px; display: inline-flex; align-items: center; gap: 5px;
background: none; border: 0; color: #1f2937; cursor: pointer;
font: inherit; padding: 4px 2px; display: inline-flex; align-items: center; gap: 5px;
}
button.link:hover { color: var(--accent); }
button.link:hover { color: #2563eb; }
button.primary {
border: 0; border-radius: 8px; padding: 8px 22px; font-weight: 650; color: #04222f;
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
box-shadow: 0 4px 14px rgba(42,164,223,.3);
background: #f4b400; color: #1a1a1a; border: 0; border-radius: 3px;
padding: 7px 22px; font-weight: 600; cursor: pointer;
}
button.primary:hover { filter: brightness(1.06); }
button:disabled { opacity: .5; cursor: default; filter: none; }
button.primary:hover { background: #e0a500; }
button:disabled { opacity: .5; cursor: default; }
main { flex: 1; overflow-y: auto; padding: 16px 18px; }
main { flex: 1; overflow-y: auto; padding: 14px 18px; }
.target { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; margin-bottom: 12px; }
.target > label { text-align: right; color: var(--muted); font-weight: 500; }
.target > label { text-align: right; color: #374151; font-weight: 500; }
/* Indexfelder: rechtsbündiges Label links, Eingabe rechts */
.fields { display: flex; flex-direction: column; gap: 9px; }
.fields { display: flex; flex-direction: column; gap: 8px; }
.field { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; }
.field > label { text-align: right; color: var(--muted); font-weight: 500; padding-top: 2px; }
.field > label { text-align: right; color: #374151; font-weight: 500; padding-top: 2px; }
.field.full { grid-template-columns: 150px 1fr; align-items: start; }
.field .req { color: var(--err); }
.field .req { color: #b91c1c; }
select, input[type="text"], input[type="number"], input[type="date"], input[type="password"], textarea {
width: 100%; padding: 8px 10px; color: var(--text);
background: var(--surface-2); border: 1px solid var(--border-strong);
border-radius: 8px; font: inherit; outline: none;
transition: border-color .15s, box-shadow .15s;
select, input[type="text"], input[type="number"], input[type="date"], textarea {
width: 100%; padding: 6px 9px; border: 1px solid #b9c0c8; border-radius: 3px; font: inherit; background: #fff;
}
select { appearance: none; padding-right: 30px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4l4 4 4-4' fill='none' stroke='%238c97a4' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
background-repeat: no-repeat; background-position: right 11px center;
}
select:focus, input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
input::placeholder { color: #5f6b78; }
textarea { resize: vertical; min-height: 60px; }
select:focus, input:focus, textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px #2563eb33; }
textarea { resize: vertical; min-height: 56px; }
/* Vorbefüllte (aus der Mail übernommene) Felder dezent kennzeichnen */
.field.prefilled input, .field.prefilled textarea, .field.prefilled select {
background: rgba(42,164,223,.08); color: #bcc8d3;
}
.field.prefilled input, .field.prefilled textarea { background: #f3f4f6; color: #4b5563; }
.options { margin: 0; }
.format { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0; }
.format-label { font-weight: 550; }
.format { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 5px 0; }
.format-label { font-weight: 500; }
.format .radio { margin: 0; }
.radio, .check { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-weight: 400; }
.radio input, .check input { width: 16px; height: 16px; accent-color: var(--accent); flex: none; }
.radio, .check { display: flex; align-items: center; gap: 8px; margin: 5px 0; font-weight: 400; }
.radio input, .check input { accent-color: #2563eb; }
.att-list { list-style: none; margin: 6px 0 0 26px; padding: 0; max-height: 110px; overflow-y: auto; }
.att-list li { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2px 0; color: var(--muted); }
.att-list li { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2px 0; color: #374151; }
/* Footer bleibt fix sichtbar, nur der Felderbereich (main) scrollt. */
footer { border-top: 1px solid var(--border); padding: 11px 18px; background: var(--surface); flex-shrink: 0; }
.muted { color: var(--muted); font-size: 12px; }
.err { color: var(--err); } .ok { color: var(--ok); }
.field.invalid input, .field.invalid textarea, .field.invalid select { border-color: var(--err); }
footer { border-top: 1px solid #e5e7eb; padding: 10px 18px; background: #fafafa; flex-shrink: 0; }
.muted { color: #6b7280; font-size: 12px; }
.err { color: #b91c1c; } .ok { color: #15803d; }
.field.invalid input, .field.invalid textarea, .field.invalid select { border-color: #b91c1c; }
/* „Bereits abgelegt"-Banner */
.banner {
display: flex; align-items: center; gap: 10px;
background: rgba(70,201,138,.12); border: 1px solid rgba(70,201,138,.4); color: #8be0b4;
border-radius: 8px; padding: 8px 11px; margin-bottom: 10px; font-size: 12px;
background: #ecfdf5; border: 1px solid #6ee7b7; color: #065f46;
border-radius: 4px; padding: 7px 10px; margin-bottom: 10px; font-size: 12px;
}
.banner[hidden] { display: none; }
.banner .banner-text { flex: 1; }
.banner .link { color: #8be0b4; text-decoration: underline; }
.banner .link:hover { color: var(--ok); }
.banner .link { color: #065f46; text-decoration: underline; }
/* Overlays: Passwort-Abfrage / Dubletten-Hinweis */
/* Passwort-Abfrage (Sitzungs-Login, nicht gespeichert) */
.overlay {
position: fixed; inset: 0; background: rgba(0, 0, 0, .55);
position: fixed; inset: 0; background: rgba(0, 0, 0, .35);
display: flex; align-items: center; justify-content: center; z-index: 50;
backdrop-filter: blur(2px);
}
.overlay[hidden] { display: none; }
.overlay-box {
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 20px 22px; width: 350px;
box-shadow: 0 18px 50px rgba(0, 0, 0, .5);
background: #fff; border-radius: 6px; padding: 18px 20px; width: 340px;
box-shadow: 0 8px 30px rgba(0, 0, 0, .25);
}
.overlay-box h2 { font-size: 15px; font-weight: 650; margin: 0 0 8px; }
.overlay-box input[type="password"] { margin-top: 8px; }
.overlay-box h2 { font-size: 15px; font-weight: 600; margin: 0 0 8px; }
.overlay-box input[type="password"] {
width: 100%; padding: 7px 9px; border: 1px solid #b9c0c8; border-radius: 3px;
font: inherit; margin-top: 8px;
}
.overlay-box input[type="password"]:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px #2563eb33; }
.overlay-box .err { min-height: 1em; margin-top: 6px; }
.overlay-actions { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 12px; }
.overlay-actions { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 10px; }
.overlay-box .pw-note { margin-top: 10px; }

View File

@ -15,7 +15,6 @@ let STATE = {
fields: [], // Felddefinitionen des Store-Dialogs
existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null)
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung)
uploadedIds: new Set(), // bereits erfolgreich abgelegte Nachrichten (für Retry-Schutz)
};
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
@ -40,17 +39,10 @@ function effectiveFieldMap() {
return m && Object.keys(m).length ? m : DEFAULT_FIELD_MAP;
}
// 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.
// Vorbefüllwert für ein DocuWare-Feld aus den Mail-Metadaten (oder undefined).
function prefillFor(fieldName, meta) {
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];
const token = effectiveFieldMap()[fieldName];
const fn = token && TOKEN_FN[token];
return fn ? fn(meta) : undefined;
}
@ -187,27 +179,11 @@ function renderAttachments() {
const ul = $("attList");
ul.innerHTML = "";
STATE.attachments.forEach((a, i) => {
// Bewusst per DOM-API (nicht innerHTML): der Anhang-Name stammt aus der Mail
// und ist angreiferkontrolliert als Text einsetzen, nie als HTML.
const li = document.createElement("li");
const cb = document.createElement("input");
cb.type = "checkbox";
cb.id = `att-${i}`;
cb.checked = true;
const label = document.createElement("label");
label.setAttribute("for", `att-${i}`);
label.style.margin = "0";
label.style.fontWeight = "400";
label.textContent = `${a.name} `;
const size = document.createElement("span");
size.className = "muted";
size.textContent = `(${formatSize(a.size)})`;
label.appendChild(size);
li.append(cb, document.createTextNode(" "), label);
li.innerHTML =
`<input type="checkbox" id="att-${i}" checked /> ` +
`<label for="att-${i}" style="margin:0;font-weight:400">${a.name} ` +
`<span class="muted">(${formatSize(a.size)})</span></label>`;
ul.appendChild(li);
});
}
@ -446,8 +422,7 @@ function buildInput(f) {
if (f.isSelectList) {
// Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook).
const dl = document.createElement("datalist");
// Feldnamen können Leer-/Sonderzeichen enthalten -> auf gültige id-Zeichen reduzieren.
dl.id = "dl-" + String(f.name).replace(/[^A-Za-z0-9_-]/g, "_");
dl.id = "dl-" + f.name;
el.setAttribute("list", dl.id);
el._datalist = dl;
}
@ -475,7 +450,7 @@ function prefillFields() {
// Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select).
function setFieldValue(name, f, val) {
const wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`);
const wrap = document.querySelector(`.field[data-name="${name}"]`);
const el = wrap && wrap.querySelector("[data-role='value']");
if (!el) return;
if (el.type === "date") {
@ -506,7 +481,7 @@ async function populateSelectLists() {
);
} catch (_) { /* leere Liste ist ok */ }
if (!vals.length) continue;
const wrap = document.querySelector(`.field[data-name="${cssEsc(f.name)}"]`);
const wrap = document.querySelector(`.field[data-name="${f.name}"]`);
const el = wrap && wrap.querySelector("[data-role='value']");
if (!el) continue;
if (el.tagName === "SELECT") {
@ -548,10 +523,7 @@ function collectFieldValues() {
if (!firstInvalid) firstInvalid = name;
}
if (String(raw).trim() !== "") {
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
// interpretiert new Date("YYYY-MM-DD") als UTC.
const value = el.type === "date" ? new Date(`${raw}T00:00:00`) : raw;
result[name] = { value, type };
result[name] = { value: el.type === "date" ? new Date(raw) : raw, type };
}
});
return { result, firstInvalid };
@ -605,8 +577,6 @@ async function submit() {
let mi = 0;
for (const mid of STATE.messageIds) {
mi++;
// Retry-Schutz: nach einem Teilfehler nicht erneut hochladen (sonst Dubletten).
if (STATE.uploadedIds.has(mid)) continue;
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
// Meta/Anhänge der jeweiligen Nachricht.
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid);
@ -655,9 +625,6 @@ async function submit() {
.sendMessage({ type: "archive:done", rfcMessageId: meta.rfcMessageId, cabinetId: STATE.cabinetId })
.catch(() => {});
}
// Diese Nachricht ist vollständig abgelegt -> bei einem späteren Retry überspringen.
STATE.uploadedIds.add(mid);
}
const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
@ -721,17 +688,7 @@ async function tagMessage(messageId) {
// --- Helfer ---
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY";
// Lokales Datum als YYYY-MM-DD (NICHT toISOString -> das rechnet auf UTC um und
// kippt eine Uhrzeit nahe Mitternacht auf den Vortag).
const toDateInput = (d) => {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
};
// Sicheres CSS-Escaping für Attribut-Selektoren (Feldnamen aus DocuWare).
const cssEsc = (s) =>
window.CSS && CSS.escape ? CSS.escape(String(s)) : String(s).replace(/["\\\]]/g, "\\$&");
const toDateInput = (d) => d.toISOString().slice(0, 10);
function formatSize(b) {
if (!b) return "";
if (b < 1024) return `${b} B`;

View File

@ -414,12 +414,7 @@ const DocuWare = {
if (t.includes("date")) {
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
const d = value instanceof Date ? value : new Date(value);
if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "Date" };
// Auf den LOKALEN Kalendertag als UTC-Mitternacht normalisieren. Sonst würde
// toISOString eine Uhrzeit nahe Mitternacht in eine andere Zeitzone und damit
// auf den Vor-/Folgetag kippen (Datumsfelder tragen keine Uhrzeit).
const utcDay = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
return { FieldName: name, Item: utcDay.toISOString(), ItemElementName: "Date" };
return { FieldName: name, Item: d.toISOString(), ItemElementName: "Date" };
}
if (t.includes("decimal") || t.includes("currency")) {
return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" };

View File

@ -29,7 +29,7 @@ const Mail = {
date: header.date instanceof Date ? header.date : new Date(header.date),
bodyText,
account,
direction: this._direction(header),
direction: this._direction(header, account),
sizeBytes: header.size || 0,
};
},
@ -140,8 +140,9 @@ const Mail = {
return "";
},
_direction(header) {
// Heuristik über den Ordnertyp; robustere Erkennung via Identitäten käme später.
_direction(header, accountName) {
// Heuristik: ist der Account-Name/eine Identität Absender -> Ausgang.
// Robustere Erkennung erfolgt später über Identitäten; v1 simpel:
const folderType = header.folder && header.folder.type;
if (folderType === "sent" || folderType === "outbox") return "Ausgang";
return "Eingang";

View File

@ -12,10 +12,7 @@ const DEFAULTS = {
mailFormat: "eml", // E-Mail wird als "eml" ODER "pdf" abgelegt (nie beides)
storeAttachments: true,
tagOnSuccess: true,
// 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.
fieldMapping: {}, // DocuWare-Feldname -> Mail-Attribut (Token). Leer = DEFAULT_FIELD_MAP.
};
// Mögliche Mail-Attribute für die Vorbefüllung (Token + Anzeigename).
@ -73,23 +70,13 @@ const Settings = {
return s;
},
// Laufende Schreibkette: serialisiert parallele set()-Aufrufe, damit zwei fast
// gleichzeitige Writes (z.B. Schrankwahl + Format-Umschalten) sich nicht beim
// read-modify-write gegenseitig überschreiben.
_writeQueue: Promise.resolve(),
async set(partial) {
const run = async () => {
const current = await this.get();
const next = { ...current, ...partial };
// Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen).
delete next.password;
await browser.storage.local.set({ settings: next });
return next;
};
const p = this._writeQueue.then(run, run);
this._writeQueue = p.catch(() => {}); // Fehler bricht die Kette nicht ab
return p;
const current = await this.get();
const next = { ...current, ...partial };
// Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen).
delete next.password;
await browser.storage.local.set({ settings: next });
return next;
},
/** Basis-URL der Platform-API ohne abschließenden Slash. */

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "DocuWare Ablage",
"description": "Legt markierte E-Mails (als .eml, PDF und mit Anhängen) in DocuWare ab.",
"version": "0.10.8",
"version": "0.10.4",
"author": "l.lingler",
"applications": {
"gecko": {

View File

@ -97,20 +97,12 @@
/* Mapping-Tabelle */
.map-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 4px 0 12px; }
.map-toolbar .grow { flex: 1; min-width: 180px; }
/* Mapping-Zeilen: Feldname + Attribut links gestapelt, fester Wert breit daneben. */
.map { margin-top: 4px; }
.map-row {
display: flex; align-items: center; gap: 10px;
background: var(--surface-2); border: 1px solid var(--border);
border-radius: 9px; padding: 8px 8px 8px 10px; margin-bottom: 8px;
}
.map-left { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; min-width: 0; }
.map-row.has-const .map-left { flex: 0 0 250px; }
.map-right { display: none; flex: 1 1 auto; min-width: 0; }
.map-row.has-const .map-right { display: flex; align-items: center; }
.map-left select, .map-right input { width: 100%; padding: 7px 9px; }
.map-hint { font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
color: var(--muted); font-weight: 600; margin: 2px 2px 8px; }
table.map { width: 100%; border-collapse: separate; border-spacing: 0 6px; }
table.map th { text-align: left; font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
color: var(--muted); font-weight: 600; padding: 0 6px 2px; }
table.map td { padding: 0 6px; vertical-align: middle; }
table.map input, table.map select { padding: 7px 9px; }
table.map td.del { width: 40px; }
.icon-btn {
width: 30px; height: 30px; padding: 0; border-radius: 7px; cursor: pointer;
background: var(--surface-2); border: 1px solid var(--border-strong); color: var(--muted);
@ -118,22 +110,6 @@
}
.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 {
@ -220,35 +196,16 @@
<span id="mapFieldStatus" class="hint" style="margin:0"></span>
</div>
<div class="map-hint">DocuWare-Feld&nbsp;→ Mail-Attribut · fester Wert</div>
<div id="mapBody" class="map"></div>
<table class="map">
<thead><tr><th>DocuWare-Feldname</th><th>Mail-Attribut</th><th></th></tr></thead>
<tbody id="mapBody"></tbody>
</table>
<datalist id="fieldNames"></datalist>
<div class="map-toolbar" style="margin-top:10px">
<button id="mapAdd" class="ghost" type="button">+ Zeile</button>
<button id="mapReset" class="ghost" type="button">Auf Standard zurücksetzen</button>
</div>
<div class="hint" style="margin-top:8px">Als Mail-Attribut „<strong>Fester Wert</strong>"
wählen, um einen festen Text (z. B. <code>NEU</code>) ins Feld zu schreiben.</div>
<details class="share">
<summary>Mapping teilen / übernehmen</summary>
<label for="mappingUrl">Aus Repo/URL laden</label>
<div class="map-toolbar">
<input type="url" id="mappingUrl" class="grow" placeholder="https://gitea.example/…/mapping.json" />
<button id="mappingPull" class="ghost" type="button">Von URL laden</button>
</div>
<div class="hint">Eine raw-JSON-Datei aus einem Gitea/GitHub-Repo. Lädt das Mapping in
den Editor mit „Speichern" übernehmen. (Lesen/Pull; Zurückschreiben ins Repo nicht.)</div>
<label for="mappingJson">JSON (kopieren / einfügen)</label>
<textarea id="mappingJson" rows="6" placeholder='{ "EML_SUBJECT": "subject", "STATUS": { "const": "NEU" } }'></textarea>
<div class="map-toolbar">
<button id="mappingExport" class="ghost" type="button">Aktuelles als JSON</button>
<button id="mappingCopy" class="ghost" type="button">Kopieren</button>
<button id="mappingApply" class="ghost" type="button">JSON anwenden</button>
</div>
</details>
</section>
</div>

View File

@ -12,40 +12,15 @@ function setStatus(msg, kind) {
el.className = kind || "";
}
// Warnt, wenn der Server über unverschlüsseltes http:// läuft dann ginge das
// Passwort im Klartext über die Leitung. Liefert einen anzuhängenden Hinweis.
function insecureWarning(url) {
return /^http:\/\//i.test(String(url || "").trim())
? " ⚠ Achtung: http:// ist unverschlüsselt bitte https:// verwenden."
: "";
}
async function load() {
const s = await Settings.get();
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 || [];
fillCabinets(cached, s.defaultCabinetId);
// Best-effort: Felder des Standard-Archivs gleich laden nur wenn schon ein
// Sitzungs-Token da ist (sonst wählt der Nutzer das Archiv manuell).
if (s.defaultCabinetId && [...$("mapCabinet").options].some((o) => o.value === s.defaultCabinetId)) {
$("mapCabinet").value = s.defaultCabinetId;
try {
const stt = await browser.runtime.sendMessage({ type: "auth:status" });
if (stt && stt.hasToken && stt.token) {
Auth.setToken(stt.token);
availableFields = (await DocuWare.cabinetFieldNames(s, s.defaultCabinetId))
.filter((n) => n && !/^DW[A-Z]/.test(n)).sort();
refreshFieldSelects();
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
}
} catch (_) { /* egal Archiv kann manuell gewählt werden */ }
}
}
function setMailFormat(v) {
@ -106,103 +81,46 @@ async function ensureToken() {
// --- Feld-Mapping-Editor ---------------------------------------------------
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
let availableFields = []; // Feldnamen aus dem zuletzt gewählten Archiv
function addMapRow(field = "", token = "") {
const tr = document.createElement("tr");
// 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: "" };
}
const inp = document.createElement("input");
inp.type = "text"; inp.className = "map-field"; inp.value = field;
inp.placeholder = "EML_SUBJECT";
inp.setAttribute("list", "fieldNames"); // Vorschläge aus gewähltem Archiv
const tdF = document.createElement("td"); tdF.appendChild(inp);
// Befüllt ein Feldname-Dropdown mit den Archiv-Feldern + dem aktuellen Wert (bleibt erhalten).
function fillFieldSelect(sel, current) {
sel.innerHTML = "";
const empty = document.createElement("option");
empty.value = ""; empty.textContent = "— Feld wählen —";
sel.appendChild(empty);
[...new Set([...(availableFields || []), current].filter(Boolean))].sort().forEach((n) => {
const o = document.createElement("option");
o.value = n; o.textContent = n;
sel.appendChild(o);
});
sel.value = current || "";
}
// Aktualisiert alle Feldname-Dropdowns auf die aktuelle Archiv-Feldliste (Auswahl bleibt).
function refreshFieldSelects() {
document.querySelectorAll("#mapBody .map-field").forEach((sel) => fillFieldSelect(sel, sel.value));
}
function addMapRow(field = "", value = "") {
const st = mapValueState(value);
const row = document.createElement("div");
row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : "");
// Links gestapelt: Feldname (Dropdown) über Mail-Attribut.
const left = document.createElement("div"); left.className = "map-left";
const fieldSel = document.createElement("select"); fieldSel.className = "map-field";
fillFieldSelect(fieldSel, field);
const src = document.createElement("select"); src.className = "map-source";
const sel = document.createElement("select"); sel.className = "map-token";
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
src.appendChild(none);
sel.appendChild(none);
MAIL_TOKENS.forEach((t) => {
const o = document.createElement("option");
o.value = t.key; o.textContent = t.label;
if (t.key === st.sel) o.selected = true;
src.appendChild(o);
});
const co = document.createElement("option");
co.value = CONST_OPT; co.textContent = "★ Fester Wert";
if (st.sel === CONST_OPT) co.selected = true;
src.appendChild(co);
left.append(fieldSel, src);
// Rechts (breit): fester Wert nur sichtbar bei „Fester Wert".
const right = document.createElement("div"); right.className = "map-right";
const konst = document.createElement("input");
konst.type = "text"; konst.className = "map-const"; konst.placeholder = "Fester Wert, z. B. NEU";
konst.value = st.konst;
right.appendChild(konst);
src.addEventListener("change", () => {
const isConst = src.value === CONST_OPT;
row.classList.toggle("has-const", isConst);
if (isConst) konst.focus();
if (t.key === token) o.selected = true;
sel.appendChild(o);
});
const tdT = document.createElement("td"); tdT.appendChild(sel);
const del = document.createElement("button");
del.type = "button"; del.className = "icon-btn"; del.textContent = "✕"; del.title = "Zeile entfernen";
del.addEventListener("click", () => row.remove());
del.addEventListener("click", () => tr.remove());
const tdX = document.createElement("td"); tdX.className = "del"; tdX.appendChild(del);
row.append(left, right, del);
$("mapBody").appendChild(row);
tr.append(tdF, tdT, tdX);
$("mapBody").appendChild(tr);
}
function loadMapping(map) {
$("mapBody").innerHTML = "";
Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v));
Object.entries(map || {}).forEach(([f, t]) => addMapRow(f, t));
}
function collectMapping() {
const map = {};
document.querySelectorAll("#mapBody .map-row").forEach((row) => {
const f = row.querySelector(".map-field").value.trim();
if (!f) return; // ohne Feldname ignorieren
const sel = row.querySelector(".map-source").value;
if (sel === CONST_OPT) {
const c = row.querySelector(".map-const").value;
if (c !== "") map[f] = { const: c }; // fester Wert
} else if (sel) {
map[f] = sel; // Mail-Token (als String, abwärtskompatibel)
}
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
});
return map;
}
@ -214,17 +132,14 @@ function collect() {
partial.defaultCabinetId = $("defaultCabinet").value;
partial.mailFormat = getMailFormat();
partial.fieldMapping = collectMapping();
partial.mappingUrl = $("mappingUrl").value.trim();
return partial;
}
// --- Events ----------------------------------------------------------------
$("save").addEventListener("click", async () => {
const data = collect();
await Settings.set(data);
const warn = insecureWarning(data.serverUrl);
setStatus("Gespeichert." + warn, warn ? "err" : "ok");
await Settings.set(collect());
setStatus("Gespeichert.", "ok");
});
$("test").addEventListener("click", async () => {
@ -238,8 +153,7 @@ $("test").addEventListener("click", async () => {
fillCabinets(cabinets, s.defaultCabinetId);
const arch = cabinets.filter((c) => !c.isBasket).length;
const bk = cabinets.filter((c) => c.isBasket).length;
const warn = insecureWarning(s.serverUrl);
setStatus(`Verbindung ok ${arch} Archive, ${bk} Briefkörbe gefunden.` + warn, warn ? "err" : "ok");
setStatus(`Verbindung ok ${arch} Archive, ${bk} Briefkörbe gefunden.`, "ok");
} catch (e) {
setStatus(`Fehler: ${e.message}`, "err");
}
@ -248,68 +162,26 @@ $("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;
const dl = $("fieldNames");
const st = $("mapFieldStatus");
if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; }
if (!id) { dl.innerHTML = ""; st.textContent = ""; return; }
st.textContent = "Lade Felder …";
try {
const s = await Settings.get();
const ok = await ensureToken();
if (!ok) { st.textContent = "Erst „Verbindung testen“ (Passwort nötig)."; return; }
availableFields = (await DocuWare.cabinetFieldNames(s, id))
.filter((n) => n && !/^DW[A-Z]/.test(n)) // DW*-Systemfelder ausblenden
.sort();
refreshFieldSelects();
st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
const names = (await DocuWare.cabinetFieldNames(s, id))
.filter((n) => n && !/^DW[A-Z]/.test(n)); // DW*-Systemfelder ausblenden
dl.innerHTML = "";
names.sort().forEach((n) => {
const o = document.createElement("option");
o.value = n;
dl.appendChild(o);
});
st.textContent = `${names.length} Felder als Vorschlag geladen.`;
} catch (e) {
st.textContent = `Fehler: ${e.message}`;
}

View File

@ -21,22 +21,6 @@
{
"version": "0.10.4",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.4/docuware-ablage-0.10.4.xpi"
},
{
"version": "0.10.5",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.5/docuware-ablage-0.10.5.xpi"
},
{
"version": "0.10.6",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.6/docuware-ablage-0.10.6.xpi"
},
{
"version": "0.10.7",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.7/docuware-ablage-0.10.7.xpi"
},
{
"version": "0.10.8",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.8/docuware-ablage-0.10.8.xpi"
}
]
}