Compare commits

..

4 Commits

Author SHA1 Message Date
e314a6af98 Release 0.12.3 2026-08-21 10:55:45 +02:00
03ff3795b2 Datum + Uhrzeit: Uhrzeit ging in Text- und DateTime-Feldern verloren
Das Token "Datum + Uhrzeit der E-Mail" lieferte nur das Datum. Drei Ursachen:

1. setFieldValue schrieb ein Date in ein TEXT-Feld ueber toDateInput, also nur
   "2026-08-04" - die Uhrzeit war weg, obwohl ein Textfeld sie problemlos traegt.
   Beim Upload wurde daraus String(dateObjekt), also die JS-Standardausgabe
   "Mon Aug 04 2026 16:28:51 GMT+0200 (...)". Neu: DocuWare.dateToText() liefert
   "04.08.2026 16:28:51", bei exakter Mitternacht nur "04.08.2026". Gilt fuer
   Text-, Memo- und Keyword-Felder und fuer Anzeige wie Upload gleichermassen.

2. TOKEN_FN.date war identisch mit dateTime (beide m.date). In einem Datums- oder
   DateTime-Feld fiel das nicht auf, in einem Textfeld schon: "Datum" haette dort
   ebenfalls eine Uhrzeit getragen. date liefert jetzt via dayOnly() den
   Kalendertag (lokale Mitternacht), wie archiveDate es schon tat.

3. Das datetime-local-Control kannte ohne step="1" nur Minuten und schnitt die
   Sekunden ab; fmtTime liefert sie jetzt mit.

Ausserdem geklaert: DateTime geht als LOKALE Wanduhrzeit ohne Zeitzonen-Suffix
raus ("2026-08-04T16:28:51") statt als toISOString() mit "Z". Ob der Server das
"Z" zurueckrechnet, haengt von dessen Zeitzone ab (Cloud meist UTC) - Zeiten
erschienen verschoben, und lokale Mitternacht kippte als UTC auf den VORTAG
22:00. Ohne Suffix ist der Wert eindeutig. Date-Felder bleiben unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:54:22 +02:00
d9ec2537e2 Release 0.12.2 2026-08-21 10:23:56 +02:00
f6c7761182 Feld-Zuordnung: Feldname wieder als Auswahl - und die Liste bleibt gefuellt
0.12.1 hatte den Feldnamen auf eine freie Texteingabe umgestellt. Das behob zwar
die Blockade, nahm aber die Auswahl weg - eigentlich war nur die Liste leer.

Jetzt wieder ein <select>, dessen Liste sich aus drei Quellen speist und damit
nie leer ist: Felder des gewaehlten Archivs, die Namen aus DEFAULT_FIELD_MAP und
alles im Formular bereits Vergebene. Ein Eintrag "Anderer Feldname" blendet eine
freie Eingabe ein, fuer Felder die in keiner Liste stehen. Ein gespeicherter Name,
den kein Archiv kennt, wird ins Dropdown aufgenommen statt verloren zu gehen.

Kern des Problems war die Herkunft der Liste: sie kam ausschliesslich vom Server
und setzte damit gecachte Schrankliste + Passwort dieser Sitzung + Archivwahl
voraus. Die Feldnamen werden nun je Archiv in storage.local unter
"fieldsByCabinet" gemerkt und beim Oeffnen VOR dem Aufbau der Zeilen gelesen -
die Auswahl steht also sofort und auch ohne Anmeldung. Ist ein Token da, wird im
Hintergrund aufgefrischt; schlaegt das fehl, bleibt die gemerkte Liste nutzbar.

Bei Server-Wechsel wird fieldsByCabinet mitgeloescht - die Feldlisten haengen an
Schrank-GUIDs des alten Servers (gleiche Logik wie fuer cabinets/defaultCabinetId).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:23:36 +02:00
6 changed files with 189 additions and 55 deletions

View File

@ -37,7 +37,10 @@ const TOKEN_FN = {
bcc: (m) => m.bcc, bcc: (m) => m.bcc,
subject: (m) => m.subject, subject: (m) => m.subject,
body: (m) => m.bodyText, body: (m) => m.bodyText,
date: (m) => m.date, // 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),
direction: (m) => m.direction, direction: (m) => m.direction,
size: (m) => m.sizeBytes, size: (m) => m.sizeBytes,
account: (m) => m.account, account: (m) => m.account,
@ -49,7 +52,7 @@ const TOKEN_FN = {
time: (m) => fmtTime(m.date), time: (m) => fmtTime(m.date),
// archiveDate ist bewusst auf Mitternacht gesetzt: so bleibt es „nur Datum", // 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. // auch wenn es auf ein DateTime-Feld gemappt wird. archiveDateTime trägt die Uhrzeit.
archiveDate: () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()); }, archiveDate: () => dayOnly(new Date()),
archiveDateTime: () => new Date(), archiveDateTime: () => new Date(),
// Dokumentbezogen // Dokumentbezogen
@ -500,6 +503,8 @@ function buildInput(f) {
// DateTime-Feld: Tag UND Uhrzeit erfassbar (vor dem reinen date-Zweig prüfen). // DateTime-Feld: Tag UND Uhrzeit erfassbar (vor dem reinen date-Zweig prüfen).
el = document.createElement("input"); el = document.createElement("input");
el.type = "datetime-local"; 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")) { } else if (t.includes("date")) {
el = document.createElement("input"); el = document.createElement("input");
el.type = "date"; el.type = "date";
@ -564,7 +569,10 @@ function setFieldValue(name, f, val) {
} }
el.value = String(val); el.value = String(val);
} else { } else {
el.value = val instanceof Date ? toDateInput(val) : String(val); // 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);
} }
wrap.classList.add("prefilled"); wrap.classList.add("prefilled");
} }
@ -859,10 +867,21 @@ const toDateInput = (d) => {
// Lokal als YYYY-MM-DDTHH:MM für <input type="datetime-local">. // Lokal als YYYY-MM-DDTHH:MM für <input type="datetime-local">.
const toDateTimeInput = (d) => `${toDateInput(d)}T${fmtTime(d)}`; const toDateTimeInput = (d) => `${toDateInput(d)}T${fmtTime(d)}`;
// Uhrzeit HH:MM lokal (auch als eigenständiger Text-Token nutzbar). // 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) { function fmtTime(d) {
const dt = d instanceof Date ? d : new Date(d); const dt = d instanceof Date ? d : new Date(d);
if (isNaN(dt)) return ""; if (isNaN(dt)) return "";
return `${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`; 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 // Dateiname, den die E-Mail selbst beim Upload bekäme – für die Vorschau im

View File

@ -427,22 +427,47 @@ const DocuWare = {
return out; 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) { _field(name, value, type) {
const t = (type || "Text").toLowerCase(); 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 // WICHTIG: "datetime" zuerst prüfen – "DateTime".includes("date") ist true und
// würde sonst die Uhrzeit auf Mitternacht normalisieren (Datenverlust). // würde sonst die Uhrzeit auf Mitternacht normalisieren (Datenverlust).
if (t.includes("datetime")) { if (t.includes("datetime")) {
const d = value instanceof Date ? value : new Date(value); const d = value instanceof Date ? value : new Date(value);
if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "DateTime" }; if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "DateTime" };
// Mit Uhrzeit: echter Zeitpunkt, daher unverändert als ISO (UTC) senden. // LOKALE Wanduhrzeit ohne Zeitzonen-Suffix ("2026-08-04T16:28:51").
// //
// UNGEPRÜFT gegen den Server: ob DocuWare das "Z" auswertet (dann stimmt die // Vorher ging hier d.toISOString() raus, also UTC mit "Z". Ob DocuWare das
// Uhrzeit) oder die ISO-Zeichenkette als lokale Wanduhrzeit übernimmt (dann // "Z" in Serverzeit zurückrechnet, hängt vom Server ab (Cloud-Instanzen
// erscheint sie um den UTC-Versatz verschoben, in Wien -2 h). Reine Date-Felder // laufen meist auf UTC) – die Uhrzeit erschien dann um den Versatz
// sind davon nicht betroffen, die werden auf UTC-Mitternacht normalisiert. // verschoben, in Wien -2 h. Schlimmer beim Kalendertag: lokale Mitternacht
// Falls im Testarchiv verschoben: hier stattdessen die lokale Wanduhrzeit ohne // wird als UTC zum VORTAG 22:00.
// Zeitzone senden, also `${toLocalIso(d)}` statt d.toISOString(). // Ohne Suffix ist der Wert eindeutig: der Server übernimmt ihn so, wie der
return { FieldName: name, Item: d.toISOString(), ItemElementName: "DateTime" }; // 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")) { if (t.includes("date")) {
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform. // DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
@ -461,13 +486,13 @@ const DocuWare = {
return { FieldName: name, Item: Number(value), ItemElementName: "Int" }; return { FieldName: name, Item: Number(value), ItemElementName: "Int" };
} }
if (t.includes("memo")) { if (t.includes("memo")) {
return { FieldName: name, Item: String(value), ItemElementName: "Memo" }; return { FieldName: name, Item: asText(value), ItemElementName: "Memo" };
} }
if (t.includes("keyword")) { if (t.includes("keyword")) {
const arr = Array.isArray(value) ? value : [String(value)]; const arr = Array.isArray(value) ? value : [asText(value)];
return { FieldName: name, Item: arr, ItemElementName: "Keywords" }; return { FieldName: name, Item: arr, ItemElementName: "Keywords" };
} }
return { FieldName: name, Item: String(value), ItemElementName: "String" }; return { FieldName: name, Item: asText(value), ItemElementName: "String" };
}, },
_isSystemField(dbName) { _isSystemField(dbName) {

View File

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

View File

@ -109,6 +109,7 @@
.map-right { display: none; flex: 1 1 auto; min-width: 0; } .map-right { display: none; flex: 1 1 auto; min-width: 0; }
.map-row.has-const .map-right { display: flex; align-items: center; } .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 select, .map-left input, .map-right input { width: 100%; padding: 7px 9px; }
.map-left input[hidden] { display: none; }
.map-hint { font-size: 11px; letter-spacing: .4px; text-transform: uppercase; .map-hint { font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
color: var(--muted); font-weight: 600; margin: 2px 2px 8px; } color: var(--muted); font-weight: 600; margin: 2px 2px 8px; }
.icon-btn { .icon-btn {
@ -222,13 +223,13 @@
<div class="map-hint">DocuWare-Feld&nbsp;→ Mail-Attribut · fester Wert</div> <div class="map-hint">DocuWare-Feld&nbsp;→ Mail-Attribut · fester Wert</div>
<div id="mapBody" class="map"></div> <div id="mapBody" class="map"></div>
<!-- Vorschlagsliste fuer alle Feldname-Eingaben (siehe refreshFieldSelects). -->
<datalist id="fieldNameList"></datalist>
<div class="map-toolbar" style="margin-top:10px"> <div class="map-toolbar" style="margin-top:10px">
<button id="mapAdd" class="ghost" type="button">+ Zeile</button> <button id="mapAdd" class="ghost" type="button">+ Zeile</button>
<button id="mapReset" class="ghost" type="button">Auf Standard zurücksetzen</button> <button id="mapReset" class="ghost" type="button">Auf Standard zurücksetzen</button>
</div> </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>" <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> wählen, um einen festen Text (z. B. <code>NEU</code>) ins Feld zu schreiben.</div>

View File

@ -58,7 +58,12 @@ async function invalidateSession(s) {
let next = s; let next = s;
if (server) { if (server) {
next = await Settings.set({ defaultCabinetId: "", dialogByCabinet: {} }); 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 fillCabinets([], ""); // Dropdowns leeren – die alte Liste gilt nicht mehr
refreshFieldSelects();
} }
snapshotAuth(next); snapshotAuth(next);
return next; return next;
@ -71,13 +76,23 @@ async function load() {
CHECKS.forEach((c) => ($(c).checked = !!s[c])); CHECKS.forEach((c) => ($(c).checked = !!s[c]));
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml"); setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
$("mappingUrl").value = s.mappingUrl || ""; $("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 // Gemerkte Schrank- und Feldlisten VOR dem Aufbau der Zeilen lesen, damit die
// Sitzungs-Token da ist (sonst wählt der Nutzer das Archiv manuell). // 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).`;
}
// Best-effort auffrischen – nur wenn schon ein Sitzungs-Token da ist. Ohne
// Token bleibt es bei der gemerkten Liste, die Auswahl funktioniert trotzdem.
if (s.defaultCabinetId && [...$("mapCabinet").options].some((o) => o.value === s.defaultCabinetId)) { if (s.defaultCabinetId && [...$("mapCabinet").options].some((o) => o.value === s.defaultCabinetId)) {
$("mapCabinet").value = s.defaultCabinetId; $("mapCabinet").value = s.defaultCabinetId;
try { try {
@ -86,10 +101,11 @@ async function load() {
Auth.setToken(stt.token); Auth.setToken(stt.token);
availableFields = (await DocuWare.cabinetFieldNames(s, s.defaultCabinetId)) availableFields = (await DocuWare.cabinetFieldNames(s, s.defaultCabinetId))
.filter((n) => n && !/^DW[A-Z]/.test(n)).sort(); .filter((n) => n && !/^DW[A-Z]/.test(n)).sort();
await rememberFields(s.defaultCabinetId, availableFields);
refreshFieldSelects(); refreshFieldSelects();
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`; $("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
} }
} catch (_) { /* egal – Archiv kann manuell gewählt werden */ } } catch (_) { /* egal – gemerkte Liste bleibt nutzbar */ }
} }
} }
@ -159,8 +175,19 @@ async function ensureToken() {
// --- Feld-Mapping-Editor --------------------------------------------------- // --- Feld-Mapping-Editor ---------------------------------------------------
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert" const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
let availableFields = []; // Feldnamen aus dem zuletzt gewählten Archiv 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(() => {});
}
// Zerlegt einen Mapping-Wert (Token-String ODER { const } / { token }) in den UI-Zustand. // Zerlegt einen Mapping-Wert (Token-String ODER { const } / { token }) in den UI-Zustand.
function mapValueState(value) { function mapValueState(value) {
@ -172,25 +199,52 @@ function mapValueState(value) {
return { sel: "", konst: "" }; return { sel: "", konst: "" };
} }
// Vorschläge für die Feldname-Eingaben: Felder des gewählten Archivs, dazu die // Feldname einer Zeile – aus dem Dropdown oder, bei „Anderer Feldname", aus der
// Standard-Feldnamen und alles schon Eingetragene. Der Archiv-Teil ist erst nach // daneben eingeblendeten Eingabe.
// Anmeldung + Archivwahl da – ohne die beiden anderen Quellen stünde man vorher function rowFieldName(row) {
// vor einer leeren Liste. 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() { function fieldSuggestions() {
const used = [...document.querySelectorAll("#mapBody .map-field")].map((i) => i.value); const used = [...document.querySelectorAll("#mapBody .map-row")].map(rowFieldName);
const all = [...(availableFields || []), ...Object.keys(DEFAULT_FIELD_MAP), ...used]; const all = [...(availableFields || []), ...Object.keys(DEFAULT_FIELD_MAP), ...used];
return [...new Set(all.filter(Boolean))].sort(); return [...new Set(all.filter(Boolean))].sort();
} }
// Baut die gemeinsame <datalist> neu auf (alle Zeilen hängen daran). // Ein Feldname-Dropdown neu aufbauen. Der aktuelle Wert steht immer mit drin,
function refreshFieldSelects() { // auch wenn das Archiv ihn nicht (mehr) kennt – sonst ginge er beim Neuaufbau
const dl = $("fieldNameList"); // still verloren.
if (!dl) return; function fillFieldSelect(sel, current) {
dl.innerHTML = ""; sel.innerHTML = "";
fieldSuggestions().forEach((n) => { const empty = document.createElement("option");
empty.value = ""; empty.textContent = "— Feld wählen —";
sel.appendChild(empty);
[...new Set([...fieldSuggestions(), current].filter(Boolean))].sort().forEach((n) => {
const o = document.createElement("option"); const o = document.createElement("option");
o.value = n; o.value = n; o.textContent = n;
dl.appendChild(o); 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.
function refreshFieldSelects() {
document.querySelectorAll("#mapBody .map-field-sel").forEach((sel) => {
if (sel.value === CUSTOM_OPT) return;
fillFieldSelect(sel, sel.value);
}); });
} }
@ -199,19 +253,33 @@ function addMapRow(field = "", value = "") {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : ""); row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : "");
// Links gestapelt: Feldname (Eingabe mit Vorschlägen) über Mail-Attribut. // Links gestapelt: Feldname (Dropdown, bei Bedarf freie Eingabe) über Mail-Attribut.
const left = document.createElement("div"); left.className = "map-left"; const left = document.createElement("div"); left.className = "map-left";
// Freie Eingabe mit Vorschlagsliste – bewusst KEIN <select>: die Archiv-Feldliste // Feldname wird ausgewählt. Die Liste ist immer gefüllt (Archiv-Cache +
// gibt es erst nach Anmeldung, ein starres Dropdown wäre bis dahin leer und die // Standardnamen), und „✎ Anderer Feldname" blendet eine freie Eingabe ein –
// Zeile unbenutzbar (ohne Feldnamen wirft collectMapping sie beim Speichern weg). // für Felder, die in keiner Liste stehen.
const fieldSel = document.createElement("input"); const fieldSel = document.createElement("select");
fieldSel.type = "text"; fieldSel.className = "map-field-sel";
fieldSel.className = "map-field";
fieldSel.setAttribute("list", "fieldNameList"); const own = document.createElement("input");
fieldSel.placeholder = "DocuWare-Feldname, z. B. EML_SUBJECT"; own.type = "text"; own.className = "map-field-custom";
fieldSel.value = field || ""; own.placeholder = "Eigener Feldname, z. B. EML_SUBJECT";
fieldSel.addEventListener("change", refreshFieldSelects); own.hidden = true;
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 src = document.createElement("select"); src.className = "map-source";
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —"; const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
@ -227,7 +295,7 @@ function addMapRow(field = "", value = "") {
if (st.sel === CONST_OPT) co.selected = true; if (st.sel === CONST_OPT) co.selected = true;
src.appendChild(co); src.appendChild(co);
left.append(fieldSel, src); left.append(fieldSel, own, src);
// Rechts (breit): fester Wert – nur sichtbar bei „Fester Wert". // Rechts (breit): fester Wert – nur sichtbar bei „Fester Wert".
const right = document.createElement("div"); right.className = "map-right"; const right = document.createElement("div"); right.className = "map-right";
@ -259,7 +327,7 @@ function loadMapping(map) {
function collectMapping() { function collectMapping() {
const map = {}; const map = {};
document.querySelectorAll("#mapBody .map-row").forEach((row) => { document.querySelectorAll("#mapBody .map-row").forEach((row) => {
const f = row.querySelector(".map-field").value.trim(); const f = rowFieldName(row);
if (!f) return; // ohne Feldname ignorieren if (!f) return; // ohne Feldname ignorieren
const sel = row.querySelector(".map-source").value; const sel = row.querySelector(".map-source").value;
if (sel === CONST_OPT) { if (sel === CONST_OPT) {
@ -372,14 +440,27 @@ $("mapCabinet").addEventListener("change", async () => {
const id = $("mapCabinet").value; const id = $("mapCabinet").value;
const st = $("mapFieldStatus"); const st = $("mapFieldStatus");
if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; } if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; }
st.textContent = "Lade Felder …";
// 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 …";
}
try { try {
const s = await Settings.get(); const s = await Settings.get();
const ok = await ensureToken(); const ok = await ensureToken();
if (!ok) { st.textContent = "Erst „Verbindung testen“ (Passwort nötig)."; return; } if (!ok) {
if (!availableFields.length) st.textContent = "Erst „Verbindung testen“ (Passwort nötig).";
return; // gemerkte Liste bleibt stehen
}
availableFields = (await DocuWare.cabinetFieldNames(s, id)) availableFields = (await DocuWare.cabinetFieldNames(s, id))
.filter((n) => n && !/^DW[A-Z]/.test(n)) // DW*-Systemfelder ausblenden .filter((n) => n && !/^DW[A-Z]/.test(n)) // DW*-Systemfelder ausblenden
.sort(); .sort();
await rememberFields(id, availableFields);
refreshFieldSelects(); refreshFieldSelects();
st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`; st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
} catch (e) { } catch (e) {

View File

@ -49,6 +49,14 @@
{ {
"version": "0.12.1", "version": "0.12.1",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.1/docuware-ablage-0.12.1.xpi" "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"
} }
] ]
} }