Compare commits

...

6 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
ecbf1fcf33 Release 0.12.1 2026-08-21 10:08:13 +02:00
2e6a1a58e5 Feld-Zuordnung: Feldname wieder frei eingebbar
Die Spalte "DocuWare-Feldname" war ein starres <select>, dessen Optionen
ausschliesslich aus availableFields kamen - der Feldliste eines Archivs. Die ist
aber erst gefuellt, wenn Schrankliste gecacht, Passwort dieser Sitzung eingegeben
und ein Archiv gewaehlt ist. Bis dahin enthielt jedes Dropdown nur
"- Feld waehlen -": eine neue Zeile hatte null auswaehlbare Felder, und weil
collectMapping() Zeilen ohne Feldnamen verwirft, verschwand sie beim Speichern
kommentarlos wieder.

Jetzt wieder freie Texteingabe mit Vorschlagsliste (<input list>), wie der
veraltete Kommentar an mapCabinet es schon beschrieb. Die Vorschlaege kommen aus
drei Quellen statt einer: Felder des gewaehlten Archivs, die Namen aus
DEFAULT_FIELD_MAP und alles bereits Eingetragene - damit stehen sofort nach dem
Oeffnen Vorschlaege bereit, auch ohne Anmeldung.

fillFieldSelect entfaellt; refreshFieldSelects baut nur noch die gemeinsame
<datalist> auf und wird zusaetzlich am Ende von loadMapping aufgerufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:07:41 +02:00
6 changed files with 189 additions and 37 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.0", "version": "0.12.3",
"author": "l.lingler", "author": "l.lingler",
"applications": { "applications": {
"gecko": { "gecko": {

View File

@ -108,7 +108,8 @@
.map-row.has-const .map-left { flex: 0 0 250px; } .map-row.has-const .map-left { flex: 0 0 250px; }
.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-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 {
@ -227,6 +228,8 @@
<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,23 +199,53 @@ function mapValueState(value) {
return { sel: "", konst: "" }; return { sel: "", konst: "" };
} }
// Befüllt ein Feldname-Dropdown mit den Archiv-Feldern + dem aktuellen Wert (bleibt erhalten). // 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.
function fillFieldSelect(sel, current) { function fillFieldSelect(sel, current) {
sel.innerHTML = ""; sel.innerHTML = "";
const empty = document.createElement("option"); const empty = document.createElement("option");
empty.value = ""; empty.textContent = "— Feld wählen —"; empty.value = ""; empty.textContent = "— Feld wählen —";
sel.appendChild(empty); sel.appendChild(empty);
[...new Set([...(availableFields || []), current].filter(Boolean))].sort().forEach((n) => { [...new Set([...fieldSuggestions(), current].filter(Boolean))].sort().forEach((n) => {
const o = document.createElement("option"); const o = document.createElement("option");
o.value = n; o.textContent = n; o.value = n; o.textContent = n;
sel.appendChild(o); sel.appendChild(o);
}); });
const own = document.createElement("option");
own.value = CUSTOM_OPT; own.textContent = "✎ Anderer Feldname …";
sel.appendChild(own);
sel.value = current || ""; sel.value = current || "";
} }
// Aktualisiert alle Feldname-Dropdowns auf die aktuelle Archiv-Feldliste (Auswahl bleibt). // Alle Dropdowns auf die aktuelle Liste bringen (Auswahl bleibt). Zeilen mit
// freier Eingabe bleiben unangetastet.
function refreshFieldSelects() { function refreshFieldSelects() {
document.querySelectorAll("#mapBody .map-field").forEach((sel) => fillFieldSelect(sel, sel.value)); document.querySelectorAll("#mapBody .map-field-sel").forEach((sel) => {
if (sel.value === CUSTOM_OPT) return;
fillFieldSelect(sel, sel.value);
});
} }
function addMapRow(field = "", value = "") { function addMapRow(field = "", value = "") {
@ -196,11 +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 (Dropdown) ü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";
const fieldSel = document.createElement("select"); fieldSel.className = "map-field"; // 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;
fillFieldSelect(fieldSel, 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 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 —";
@ -216,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";
@ -242,12 +321,13 @@ function addMapRow(field = "", value = "") {
function loadMapping(map) { function loadMapping(map) {
$("mapBody").innerHTML = ""; $("mapBody").innerHTML = "";
Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v)); Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v));
refreshFieldSelects();
} }
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) {
@ -355,19 +435,32 @@ $("mappingPull").addEventListener("click", async () => {
} }
}); });
// Archiv wählen -> dessen Indexfelder als Vorschläge (datalist) für alle Feldnamen. // Archiv wählen -> dessen Indexfelder als Vorschläge für alle Feldname-Eingaben.
$("mapCabinet").addEventListener("change", async () => { $("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

@ -45,6 +45,18 @@
{ {
"version": "0.12.0", "version": "0.12.0",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.0/docuware-ablage-0.12.0.xpi" "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"
} }
] ]
} }