Compare commits

..

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

8 changed files with 46 additions and 358 deletions

View File

@ -27,8 +27,6 @@ let STATE = {
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
// Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap).
// Zweites Argument ctx trägt Werte, die nicht an der Mail hängen, sondern am
// konkreten Upload: { fileName, attachmentCount }.
const TOKEN_FN = {
senderEmail: (m) => m.senderEmail,
senderName: (m) => m.senderName,
@ -37,41 +35,12 @@ const TOKEN_FN = {
bcc: (m) => m.bcc,
subject: (m) => m.subject,
body: (m) => m.bodyText,
// Nur der Kalendertag: auf lokale Mitternacht gesetzt, damit „Datum" auch in
// einem DateTime- oder Textfeld keine Uhrzeit trägt (gleiche Konvention wie
// archiveDate). Den echten Zeitpunkt liefert dateTime.
date: (m) => dayOnly(m.date),
date: (m) => m.date,
direction: (m) => m.direction,
size: (m) => m.sizeBytes,
account: (m) => m.account,
// Datum/Zeit. date liefert den Kalendertag, dateTime den echten Zeitpunkt –
// in DocuWare braucht letzteres ein Feld vom Typ DateTime, sonst schneidet
// der Server die Uhrzeit ab.
dateTime: (m) => m.date,
time: (m) => fmtTime(m.date),
// archiveDate ist bewusst auf Mitternacht gesetzt: so bleibt es „nur Datum",
// auch wenn es auf ein DateTime-Feld gemappt wird. archiveDateTime trägt die Uhrzeit.
archiveDate: () => dayOnly(new Date()),
archiveDateTime: () => new Date(),
// Dokumentbezogen
fileName: (m, ctx) => (ctx && ctx.fileName) || "",
attachmentCount: (m, ctx) => (ctx && ctx.attachmentCount) || 0,
rfcMessageId: (m) => DocuWare.normalizeMessageId(m.rfcMessageId),
// Wer/Woher – aus den Einstellungen bzw. dem Thunderbird-Konto.
dwUser: () => (STATE.settings && STATE.settings.username) || "",
dwOrganization: () => (STATE.settings && STATE.settings.organization) || "",
accountEmail: (m) => m.accountEmail || "",
folder: (m) => m.folder || "",
};
// Tokens, deren Wert vom einzelnen hochgeladenen Dokument abhängt. Felder, die
// darauf gemappt sind, werden je Upload neu gesetzt – sonst trüge jeder Anhang
// den Dateinamen der E-Mail.
const PER_FILE_TOKENS = new Set(["fileName"]);
// Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default.
function effectiveFieldMap() {
const m = STATE.settings && STATE.settings.fieldMapping;
@ -80,23 +49,16 @@ function effectiveFieldMap() {
// 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, ctx) {
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, ctx);
if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta);
return undefined;
}
const fn = TOKEN_FN[m];
return fn ? fn(meta, ctx) : undefined;
}
// Auf welchen Token ist ein Feld gemappt? (String-Form oder { token }.)
function tokenOf(fieldName) {
const m = effectiveFieldMap()[fieldName];
if (typeof m === "string") return m;
return m && typeof m === "object" ? m.token : undefined;
return fn ? fn(meta) : undefined;
}
function status(msg, kind) {
@ -499,12 +461,6 @@ function buildInput(f) {
// Nur Listenwerte erlaubt -> starres Dropdown.
el = document.createElement("select");
el.innerHTML = '<option value=""></option>';
} else if (t.includes("datetime")) {
// DateTime-Feld: Tag UND Uhrzeit erfassbar (vor dem reinen date-Zweig prüfen).
el = document.createElement("input");
el.type = "datetime-local";
// Ohne step="1" kennt das Control nur Minuten und schneidet die Sekunden ab.
el.step = "1";
} else if (t.includes("date")) {
el = document.createElement("input");
el.type = "date";
@ -537,18 +493,10 @@ function applyFieldDefaults() {
}
function prefillFields() {
const ctx = formCtx();
STATE.fields.forEach((f) => {
const val = prefillFor(f.name, STATE.meta, ctx);
const val = prefillFor(f.name, STATE.meta);
if (val === undefined || val === null || val === "") return;
setFieldValue(f.name, f, val);
// Dateiname-Felder sind nur eine Vorschau: beim Ablegen trägt jeder Anhang
// seinen eigenen Namen. Im Tooltip erklären, statt still zu überschreiben.
if (PER_FILE_TOKENS.has(tokenOf(f.name))) {
const wrap = document.querySelector(`.field[data-name="${cssEsc(f.name)}"]`);
const el = wrap && wrap.querySelector("[data-role='value']");
if (el) el.title = "Wird beim Ablegen je Dokument gesetzt (Anhänge tragen ihren eigenen Dateinamen).";
}
});
}
@ -557,9 +505,9 @@ function setFieldValue(name, f, val) {
const wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`);
const el = wrap && wrap.querySelector("[data-role='value']");
if (!el) return;
if (el.type === "date" || el.type === "datetime-local") {
if (el.type === "date") {
const d = val instanceof Date ? val : new Date(val);
if (!isNaN(d)) el.value = el.type === "date" ? toDateInput(d) : toDateTimeInput(d);
if (!isNaN(d)) el.value = toDateInput(d);
} else if (el.tagName === "SELECT") {
let opt = Array.from(el.options).find((o) => o.value === String(val));
if (!opt) {
@ -569,10 +517,7 @@ function setFieldValue(name, f, val) {
}
el.value = String(val);
} else {
// Textfeld: Datum lesbar ausschreiben ("04.08.2026 16:28:51"). Früher lief
// hier toDateInput – das warf die Uhrzeit weg, obwohl ein Textfeld sie
// problemlos trägt (genau der Fall „Datum + Uhrzeit" ohne Uhrzeit).
el.value = val instanceof Date ? DocuWare.dateToText(val) : String(val);
el.value = val instanceof Date ? toDateInput(val) : String(val);
}
wrap.classList.add("prefilled");
}
@ -631,12 +576,8 @@ function collectFieldValues() {
}
if (String(raw).trim() !== "") {
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
// interpretiert new Date("YYYY-MM-DD") als UTC. datetime-local ist bereits
// lokal und wird direkt geparst.
const value =
el.type === "date" ? new Date(`${raw}T00:00:00`)
: el.type === "datetime-local" ? new Date(raw)
: raw;
// interpretiert new Date("YYYY-MM-DD") als UTC.
const value = el.type === "date" ? new Date(`${raw}T00:00:00`) : raw;
result[name] = { value, type };
}
});
@ -702,8 +643,7 @@ async function submit() {
mid === STATE.messageId ? STATE.attachments : await Mail.listAttachments(mid);
// Indexfelder: manuelle Werte für alle gleich; gemappte Felder je Mail neu.
const msgCtx = { attachmentCount: atts.length };
const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi, msgCtx);
const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi);
// Message-ID für Dedup/„bereits abgelegt" mitschreiben (wenn Schrank EML_ID hat).
if (!STATE.isBasket && meta.rfcMessageId && fieldNames.includes("EML_ID")) {
fields.EML_ID = { value: DocuWare.normalizeMessageId(meta.rfcMessageId), type: "Text" };
@ -714,16 +654,14 @@ async function submit() {
await uploadOnce(`${mid}|eml`, async () => {
status(`${prefix}E-Mail …`);
const eml = await Mail.getEmlFile(mid, meta.subject);
const ef = applyPerFileFields(fields, meta, { ...msgCtx, fileName: eml.name });
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, ef);
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields);
});
}
if (wantPdf) {
await uploadOnce(`${mid}|pdf`, async () => {
status(`${prefix}PDF …`);
const pdf = Pdf.fromMail(meta);
const pf = applyPerFileFields(fields, meta, { ...msgCtx, fileName: pdf.name });
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, pf);
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields);
});
}
if (wantAtt) {
@ -735,14 +673,7 @@ async function submit() {
await uploadOnce(`${mid}|att:${a.partName}`, async () => {
status(`${prefix}Anhang ${a.name} …`);
const file = await Mail.getAttachmentFile(mid, a.partName);
// Anhang trägt seinen EIGENEN Dateinamen – sowohl in gemappten
// fileName-Feldern als auch im festen DOC_FILE_NAME.
const af = STATE.isBasket
? {}
: {
...applyPerFileFields(fields, meta, { ...msgCtx, fileName: a.name }),
DOC_FILE_NAME: { value: a.name, type: "Text" },
};
const af = STATE.isBasket ? {} : { ...fields, DOC_FILE_NAME: { value: a.name, type: "Text" } };
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
});
}
@ -789,32 +720,17 @@ async function uploadOnce(part, doUpload) {
// Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch)
// werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben.
function fieldsForMessage(baseResult, meta, multi, ctx) {
function fieldsForMessage(baseResult, meta, multi) {
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
const result = { ...baseResult };
STATE.fields.forEach((f) => {
const val = prefillFor(f.name, meta, ctx);
const val = prefillFor(f.name, meta);
if (val === undefined || val === null || val === "") return;
result[f.name] = { value: val, type: f.type };
});
return result;
}
// Felder, die auf ein dokumentabhängiges Token (fileName) gemappt sind, für
// GENAU dieses Dokument setzen – auch bei Einzelmail, weil ein Formularwert
// nicht gleichzeitig für .eml und jeden Anhang stimmen kann.
function applyPerFileFields(fields, meta, ctx) {
if (STATE.isBasket) return fields; // Briefkorb: keine Indexfelder
const out = { ...fields };
STATE.fields.forEach((f) => {
if (!PER_FILE_TOKENS.has(tokenOf(f.name))) return;
const val = prefillFor(f.name, meta, ctx);
if (val === undefined || val === null || val === "") return;
out[f.name] = { value: val, type: f.type };
});
return out;
}
// Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken.
async function saveOptionPrefs(mailFormat, storeAttachments) {
try {
@ -864,38 +780,6 @@ const toDateInput = (d) => {
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
};
// Lokal als YYYY-MM-DDTHH:MM für <input type="datetime-local">.
const toDateTimeInput = (d) => `${toDateInput(d)}T${fmtTime(d)}`;
// Uhrzeit HH:MM lokal (auch als eigenständiger Text-Token nutzbar).
// Kalendertag ohne Uhrzeit (lokale Mitternacht). Funktionsdeklaration, damit
// TOKEN_FN sie oben im Modul schon verwenden kann.
function dayOnly(d) {
const dt = d instanceof Date ? d : new Date(d);
if (isNaN(dt)) return dt;
return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate());
}
// Uhrzeit inkl. Sekunden ("16:28:51"). Die Sekunden braucht auch
// toDateTimeInput – ein datetime-local mit step="1" nimmt sie entgegen.
function fmtTime(d) {
const dt = d instanceof Date ? d : new Date(d);
if (isNaN(dt)) return "";
const p = (n) => String(n).padStart(2, "0");
return `${p(dt.getHours())}:${p(dt.getMinutes())}:${p(dt.getSeconds())}`;
}
// Dateiname, den die E-Mail selbst beim Upload bekäme – für die Vorschau im
// Formular. Beim tatsächlichen Upload gilt der echte Name (siehe submit()).
function previewFileName() {
const subj = (STATE.meta && STATE.meta.subject) || "";
const base = subj.replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80).trim() || "email";
return `${base}.${getMailFormat() === "pdf" ? "pdf" : "eml"}`;
}
// Kontext für die Token-Auflösung im Formular (Vorschauwerte).
function formCtx() {
return { fileName: previewFileName(), attachmentCount: STATE.attachments.length };
}
// 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, "\\$&");
@ -927,21 +811,8 @@ $("cancel").addEventListener("click", () => window.close());
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
r.addEventListener("change", () => {
refreshPerFilePreview(); // .eml ↔ .pdf: Dateiname-Vorschau nachziehen
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
})
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
);
// Aktualisiert die Vorschauwerte dokumentabhängiger Felder (Dateiname).
function refreshPerFilePreview() {
const ctx = formCtx();
STATE.fields.forEach((f) => {
if (!PER_FILE_TOKENS.has(tokenOf(f.name))) return;
const val = prefillFor(f.name, STATE.meta, ctx);
if (val !== undefined && val !== null && val !== "") setFieldValue(f.name, f, val);
});
}
$("optAtt").addEventListener("change", () => {
const show = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0;
$("attList").style.display = show ? "" : "none";

View File

@ -427,48 +427,8 @@ const DocuWare = {
return out;
},
/**
* Date-Objekt als deutscher Text: "04.08.2026 16:28:51". Ohne Uhrzeit (exakt
* Mitternacht) nur "04.08.2026" – so tragen reine Datums-Tokens kein
* künstliches " 00:00:00". Gebraucht, sobald ein Datum in einem TEXT-Feld
* landet: String(dateObjekt) lieferte sonst die JS-Standardausgabe
* ("Mon Aug 04 2026 16:28:51 GMT+0200 (Mitteleuropäische Sommerzeit)").
* Bewusst lokale Zeit ohne UTC-Umrechnung – der Text soll zeigen, was der
* Nutzer in Thunderbird sieht.
*/
dateToText(value) {
const d = value instanceof Date ? value : new Date(value);
if (isNaN(d)) return String(value);
const p = (n) => String(n).padStart(2, "0");
const day = `${p(d.getDate())}.${p(d.getMonth() + 1)}.${d.getFullYear()}`;
if (!d.getHours() && !d.getMinutes() && !d.getSeconds()) return day;
return `${day} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
_field(name, value, type) {
const t = (type || "Text").toLowerCase();
// Datum in einem Text-/Memo-Feld lesbar machen (s. dateToText).
const asText = (v) => (v instanceof Date ? this.dateToText(v) : String(v));
// WICHTIG: "datetime" zuerst prüfen – "DateTime".includes("date") ist true und
// würde sonst die Uhrzeit auf Mitternacht normalisieren (Datenverlust).
if (t.includes("datetime")) {
const d = value instanceof Date ? value : new Date(value);
if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "DateTime" };
// LOKALE Wanduhrzeit ohne Zeitzonen-Suffix ("2026-08-04T16:28:51").
//
// Vorher ging hier d.toISOString() raus, also UTC mit "Z". Ob DocuWare das
// "Z" in Serverzeit zurückrechnet, hängt vom Server ab (Cloud-Instanzen
// laufen meist auf UTC) – die Uhrzeit erschien dann um den Versatz
// verschoben, in Wien -2 h. Schlimmer beim Kalendertag: lokale Mitternacht
// wird als UTC zum VORTAG 22:00.
// Ohne Suffix ist der Wert eindeutig: der Server übernimmt ihn so, wie der
// Nutzer die Zeit in Thunderbird sieht. Reine Date-Felder bleiben unberührt.
const p = (n) => String(n).padStart(2, "0");
const iso =
`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` +
`T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
return { FieldName: name, Item: iso, ItemElementName: "DateTime" };
}
if (t.includes("date")) {
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
const d = value instanceof Date ? value : new Date(value);
@ -486,13 +446,13 @@ const DocuWare = {
return { FieldName: name, Item: Number(value), ItemElementName: "Int" };
}
if (t.includes("memo")) {
return { FieldName: name, Item: asText(value), ItemElementName: "Memo" };
return { FieldName: name, Item: String(value), ItemElementName: "Memo" };
}
if (t.includes("keyword")) {
const arr = Array.isArray(value) ? value : [asText(value)];
const arr = Array.isArray(value) ? value : [String(value)];
return { FieldName: name, Item: arr, ItemElementName: "Keywords" };
}
return { FieldName: name, Item: asText(value), ItemElementName: "String" };
return { FieldName: name, Item: String(value), ItemElementName: "String" };
},
_isSystemField(dbName) {

View File

@ -16,11 +16,9 @@ const Mail = {
const bodyText = this._extractText(full);
const sender = this._parseAddress(header.author);
const acc = await this._accountInfo(messageId);
const account = await this._accountForMessage(messageId);
return {
folder: (header.folder && (header.folder.name || header.folder.path)) || "",
accountEmail: acc.email,
rfcMessageId: this._header(full, "message-id"), // RFC822 Message-ID (Dedup-Schlüssel)
subject: header.subject || "",
senderEmail: sender.email,
@ -30,7 +28,7 @@ const Mail = {
bcc: (header.bccList || []).join(", "),
date: header.date instanceof Date ? header.date : new Date(header.date),
bodyText,
account: acc.name,
account,
direction: this._direction(header),
sizeBytes: header.size || 0,
};
@ -130,23 +128,16 @@ const Mail = {
return { name: "", email: str.trim() };
},
/**
* Konto-Name und eigene E-Mail-Adresse (erste Identität) zur Nachricht.
* @returns {Promise<{name:string, email:string}>}
*/
async _accountInfo(messageId) {
async _accountForMessage(messageId) {
try {
const msg = await browser.messages.get(messageId);
const folder = msg.folder;
if (folder && folder.accountId) {
const acc = await browser.accounts.get(folder.accountId);
if (acc) {
const ident = (acc.identities || [])[0];
return { name: acc.name || "", email: (ident && ident.email) || "" };
}
return acc ? acc.name : "";
}
} catch (_) {}
return { name: "", email: "" };
return "";
},
_direction(header) {

View File

@ -32,27 +32,6 @@ const MAIL_TOKENS = [
{ key: "direction", label: "Richtung (Ein-/Ausgang)" },
{ key: "size", label: "Größe (Bytes)" },
{ key: "account", label: "Konto" },
// --- Datum/Zeit ---------------------------------------------------------
// "date" liefert nur den Kalendertag. Für DocuWare-Felder vom Typ DateTime
// gibt es "dateTime" (Tag + Uhrzeit); "time" nur die Uhrzeit als Text.
{ key: "dateTime", label: "Datum + Uhrzeit der E-Mail" },
{ key: "time", label: "Uhrzeit der E-Mail" },
{ key: "archiveDate", label: "Ablagedatum (heute)" },
{ key: "archiveDateTime", label: "Ablagezeitpunkt (jetzt)" },
// --- Dokument -----------------------------------------------------------
// fileName wird JE HOCHGELADENEM DOKUMENT gesetzt: .eml/PDF tragen den
// Mailnamen, jeder Anhang seinen eigenen Dateinamen.
{ key: "fileName", label: "Dateiname (je Dokument)" },
{ key: "attachmentCount", label: "Anzahl Anhänge" },
{ key: "rfcMessageId", label: "Message-ID (RFC822)" },
// --- Wer/Woher ----------------------------------------------------------
{ key: "dwUser", label: "DocuWare-Benutzer (Mitarbeiter)" },
{ key: "dwOrganization", label: "DocuWare-Organisation" },
{ key: "accountEmail", label: "Eigene E-Mail-Adresse" },
{ key: "folder", label: "Ordner" },
];
// Standard-Zuordnung DocuWare-Feldname -> Mail-Token. Greift, solange der Nutzer
@ -74,7 +53,6 @@ const DEFAULT_FIELD_MAP = {
EML_SIZE: "size",
EML_ACCOUNT: "account",
DOC_DATE: "date",
DOC_FILE_NAME: "fileName", // .eml/PDF: Mailname, Anhang: eigener Dateiname
SUBJECT: "subject",
BETREFF: "subject",
EMAIL: "senderEmail",

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.12.3",
"version": "0.11.0",
"author": "l.lingler",
"applications": {
"gecko": {

View File

@ -108,8 +108,7 @@
.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-left input, .map-right input { width: 100%; padding: 7px 9px; }
.map-left input[hidden] { display: none; }
.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; }
.icon-btn {
@ -228,8 +227,6 @@
<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">Steht ein Feld nicht in der Liste,
„<strong>✎ Anderer Feldname</strong>" wählen und den Namen eintippen.</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>

View File

@ -58,12 +58,7 @@ async function invalidateSession(s) {
let next = s;
if (server) {
next = await Settings.set({ defaultCabinetId: "", dialogByCabinet: {} });
// Gemerkte Feldlisten hängen an Schrank-GUIDs des alten Servers -> weg damit.
fieldsByCabinet = {};
await browser.storage.local.remove("fieldsByCabinet").catch(() => {});
availableFields = [];
fillCabinets([], ""); // Dropdowns leeren – die alte Liste gilt nicht mehr
refreshFieldSelects();
}
snapshotAuth(next);
return next;
@ -76,23 +71,13 @@ async function load() {
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
$("mappingUrl").value = s.mappingUrl || "";
// Gemerkte Schrank- und Feldlisten VOR dem Aufbau der Zeilen lesen, damit die
// Feldname-Dropdowns schon beim ersten Rendern gefüllt sind.
const local = await browser.storage.local.get(["cabinets", "fieldsByCabinet"]);
fieldsByCabinet = local.fieldsByCabinet || {};
if (s.defaultCabinetId && Array.isArray(fieldsByCabinet[s.defaultCabinetId])) {
availableFields = fieldsByCabinet[s.defaultCabinetId];
}
loadMapping(s.fieldMapping && Object.keys(s.fieldMapping).length ? s.fieldMapping : DEFAULT_FIELD_MAP);
fillCabinets(local.cabinets || [], s.defaultCabinetId);
if (availableFields.length) {
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl (gemerkt).`;
}
// Schrankliste, falls vorher schon getestet (im Storage gecached)
const cached = (await browser.storage.local.get("cabinets")).cabinets || [];
fillCabinets(cached, s.defaultCabinetId);
// Best-effort auffrischen – nur wenn schon ein Sitzungs-Token da ist. Ohne
// Token bleibt es bei der gemerkten Liste, die Auswahl funktioniert trotzdem.
// 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 {
@ -101,11 +86,10 @@ async function load() {
Auth.setToken(stt.token);
availableFields = (await DocuWare.cabinetFieldNames(s, s.defaultCabinetId))
.filter((n) => n && !/^DW[A-Z]/.test(n)).sort();
await rememberFields(s.defaultCabinetId, availableFields);
refreshFieldSelects();
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
}
} catch (_) { /* egal – gemerkte Liste bleibt nutzbar */ }
} catch (_) { /* egal – Archiv kann manuell gewählt werden */ }
}
}
@ -175,19 +159,8 @@ async function ensureToken() {
// --- Feld-Mapping-Editor ---------------------------------------------------
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
const CUSTOM_OPT = "__custom__"; // Sentinel im Feldname-Dropdown für „eigener Name"
let availableFields = []; // Feldnamen des zuletzt gewählten Archivs
// Gemerkte Feldnamen je Archiv (storage.local, Schlüssel "fieldsByCabinet").
// Ohne diesen Cache war die Auswahlliste bei jedem Thunderbird-Start wieder leer:
// sie kam nur vom Server und setzte damit Passworteingabe + Archivwahl voraus.
let fieldsByCabinet = {};
async function rememberFields(cabinetId, names) {
fieldsByCabinet = { ...fieldsByCabinet, [cabinetId]: names };
await browser.storage.local.set({ fieldsByCabinet }).catch(() => {});
}
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
let availableFields = []; // Feldnamen aus dem zuletzt gewählten Archiv
// Zerlegt einen Mapping-Wert (Token-String ODER { const } / { token }) in den UI-Zustand.
function mapValueState(value) {
@ -199,53 +172,23 @@ function mapValueState(value) {
return { sel: "", konst: "" };
}
// Feldname einer Zeile – aus dem Dropdown oder, bei „Anderer Feldname", aus der
// daneben eingeblendeten Eingabe.
function rowFieldName(row) {
const sel = row.querySelector(".map-field-sel");
if (!sel) return "";
if (sel.value === CUSTOM_OPT) {
const inp = row.querySelector(".map-field-custom");
return inp ? inp.value.trim() : "";
}
return sel.value.trim();
}
// Auswahlliste für die Feldname-Dropdowns. Drei Quellen, damit sie NIE leer ist:
// Felder des gewählten Archivs (auch aus dem Cache, also ohne Anmeldung), die
// Standard-Feldnamen und alles im Formular bereits Vergebene.
function fieldSuggestions() {
const used = [...document.querySelectorAll("#mapBody .map-row")].map(rowFieldName);
const all = [...(availableFields || []), ...Object.keys(DEFAULT_FIELD_MAP), ...used];
return [...new Set(all.filter(Boolean))].sort();
}
// Ein Feldname-Dropdown neu aufbauen. Der aktuelle Wert steht immer mit drin,
// auch wenn das Archiv ihn nicht (mehr) kennt – sonst ginge er beim Neuaufbau
// still verloren.
// 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([...fieldSuggestions(), current].filter(Boolean))].sort().forEach((n) => {
[...new Set([...(availableFields || []), current].filter(Boolean))].sort().forEach((n) => {
const o = document.createElement("option");
o.value = n; o.textContent = n;
sel.appendChild(o);
});
const own = document.createElement("option");
own.value = CUSTOM_OPT; own.textContent = "✎ Anderer Feldname …";
sel.appendChild(own);
sel.value = current || "";
}
// Alle Dropdowns auf die aktuelle Liste bringen (Auswahl bleibt). Zeilen mit
// freier Eingabe bleiben unangetastet.
// Aktualisiert alle Feldname-Dropdowns auf die aktuelle Archiv-Feldliste (Auswahl bleibt).
function refreshFieldSelects() {
document.querySelectorAll("#mapBody .map-field-sel").forEach((sel) => {
if (sel.value === CUSTOM_OPT) return;
fillFieldSelect(sel, sel.value);
});
document.querySelectorAll("#mapBody .map-field").forEach((sel) => fillFieldSelect(sel, sel.value));
}
function addMapRow(field = "", value = "") {
@ -253,33 +196,11 @@ function addMapRow(field = "", value = "") {
const row = document.createElement("div");
row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : "");
// Links gestapelt: Feldname (Dropdown, bei Bedarf freie Eingabe) über Mail-Attribut.
// Links gestapelt: Feldname (Dropdown) über Mail-Attribut.
const left = document.createElement("div"); left.className = "map-left";
// Feldname wird ausgewählt. Die Liste ist immer gefüllt (Archiv-Cache +
// Standardnamen), und „✎ Anderer Feldname" blendet eine freie Eingabe ein –
// für Felder, die in keiner Liste stehen.
const fieldSel = document.createElement("select");
fieldSel.className = "map-field-sel";
const own = document.createElement("input");
own.type = "text"; own.className = "map-field-custom";
own.placeholder = "Eigener Feldname, z. B. EML_SUBJECT";
own.hidden = true;
const fieldSel = document.createElement("select"); fieldSel.className = "map-field";
fillFieldSelect(fieldSel, field);
if (field && fieldSel.value !== field) {
// Sicherheitsnetz: Name nicht im Dropdown gelandet -> als freie Eingabe zeigen,
// statt ihn stillschweigend zu verlieren.
fieldSel.value = CUSTOM_OPT; own.value = field; own.hidden = false;
}
fieldSel.addEventListener("change", () => {
const isOwn = fieldSel.value === CUSTOM_OPT;
own.hidden = !isOwn;
if (isOwn) own.focus();
});
own.addEventListener("change", refreshFieldSelects);
const src = document.createElement("select"); src.className = "map-source";
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
@ -295,7 +216,7 @@ function addMapRow(field = "", value = "") {
if (st.sel === CONST_OPT) co.selected = true;
src.appendChild(co);
left.append(fieldSel, own, src);
left.append(fieldSel, src);
// Rechts (breit): fester Wert – nur sichtbar bei „Fester Wert".
const right = document.createElement("div"); right.className = "map-right";
@ -321,13 +242,12 @@ function addMapRow(field = "", value = "") {
function loadMapping(map) {
$("mapBody").innerHTML = "";
Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v));
refreshFieldSelects();
}
function collectMapping() {
const map = {};
document.querySelectorAll("#mapBody .map-row").forEach((row) => {
const f = rowFieldName(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) {
@ -435,32 +355,19 @@ $("mappingPull").addEventListener("click", async () => {
}
});
// Archiv wählen -> dessen Indexfelder als Vorschläge für alle Feldname-Eingaben.
// Archiv wählen -> dessen Indexfelder als Vorschläge (datalist) für alle Feldnamen.
$("mapCabinet").addEventListener("change", async () => {
const id = $("mapCabinet").value;
const st = $("mapFieldStatus");
if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; }
// Gemerkte Liste sofort anzeigen – dann ist die Auswahl auch ohne Anmeldung da.
if (Array.isArray(fieldsByCabinet[id])) {
availableFields = fieldsByCabinet[id];
refreshFieldSelects();
st.textContent = `${availableFields.length} Felder zur Auswahl (gemerkt).`;
} else {
st.textContent = "Lade Felder …";
}
st.textContent = "Lade Felder …";
try {
const s = await Settings.get();
const ok = await ensureToken();
if (!ok) {
if (!availableFields.length) st.textContent = "Erst „Verbindung testen“ (Passwort nötig).";
return; // gemerkte Liste bleibt stehen
}
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();
await rememberFields(id, availableFields);
refreshFieldSelects();
st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
} catch (e) {

View File

@ -41,22 +41,6 @@
{
"version": "0.11.0",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.11.0/docuware-ablage-0.11.0.xpi"
},
{
"version": "0.12.0",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.0/docuware-ablage-0.12.0.xpi"
},
{
"version": "0.12.1",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.1/docuware-ablage-0.12.1.xpi"
},
{
"version": "0.12.2",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.2/docuware-ablage-0.12.2.xpi"
},
{
"version": "0.12.3",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.3/docuware-ablage-0.12.3.xpi"
}
]
}