Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e314a6af98 | |||
| 03ff3795b2 | |||
| d9ec2537e2 | |||
| f6c7761182 | |||
| ecbf1fcf33 | |||
| 2e6a1a58e5 | |||
| cbaee51b4e | |||
| 7e351a818a |
163
dialog/dialog.js
163
dialog/dialog.js
@ -27,6 +27,8 @@ let STATE = {
|
|||||||
|
|
||||||
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
|
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
|
||||||
// Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap).
|
// 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 = {
|
const TOKEN_FN = {
|
||||||
senderEmail: (m) => m.senderEmail,
|
senderEmail: (m) => m.senderEmail,
|
||||||
senderName: (m) => m.senderName,
|
senderName: (m) => m.senderName,
|
||||||
@ -35,12 +37,41 @@ 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,
|
||||||
|
|
||||||
|
// 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.
|
// Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default.
|
||||||
function effectiveFieldMap() {
|
function effectiveFieldMap() {
|
||||||
const m = STATE.settings && STATE.settings.fieldMapping;
|
const m = STATE.settings && STATE.settings.fieldMapping;
|
||||||
@ -49,16 +80,23 @@ function effectiveFieldMap() {
|
|||||||
|
|
||||||
// Vorbefüllwert für ein DocuWare-Feld (oder undefined). Das Mapping kann je Feld
|
// 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.
|
// entweder einen Mail-Token (String) ODER einen festen Wert ({ const: "NEU" }) tragen.
|
||||||
function prefillFor(fieldName, meta) {
|
function prefillFor(fieldName, meta, ctx) {
|
||||||
const m = effectiveFieldMap()[fieldName];
|
const m = effectiveFieldMap()[fieldName];
|
||||||
if (!m) return undefined;
|
if (!m) return undefined;
|
||||||
if (typeof m === "object") {
|
if (typeof m === "object") {
|
||||||
if (m.const !== undefined && m.const !== null && m.const !== "") return m.const;
|
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);
|
if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta, ctx);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const fn = TOKEN_FN[m];
|
const fn = TOKEN_FN[m];
|
||||||
return fn ? fn(meta) : undefined;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function status(msg, kind) {
|
function status(msg, kind) {
|
||||||
@ -461,6 +499,12 @@ function buildInput(f) {
|
|||||||
// Nur Listenwerte erlaubt -> starres Dropdown.
|
// Nur Listenwerte erlaubt -> starres Dropdown.
|
||||||
el = document.createElement("select");
|
el = document.createElement("select");
|
||||||
el.innerHTML = '<option value=""></option>';
|
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")) {
|
} else if (t.includes("date")) {
|
||||||
el = document.createElement("input");
|
el = document.createElement("input");
|
||||||
el.type = "date";
|
el.type = "date";
|
||||||
@ -493,10 +537,18 @@ function applyFieldDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prefillFields() {
|
function prefillFields() {
|
||||||
|
const ctx = formCtx();
|
||||||
STATE.fields.forEach((f) => {
|
STATE.fields.forEach((f) => {
|
||||||
const val = prefillFor(f.name, STATE.meta);
|
const val = prefillFor(f.name, STATE.meta, ctx);
|
||||||
if (val === undefined || val === null || val === "") return;
|
if (val === undefined || val === null || val === "") return;
|
||||||
setFieldValue(f.name, f, val);
|
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).";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,9 +557,9 @@ function setFieldValue(name, f, val) {
|
|||||||
const wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`);
|
const wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`);
|
||||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (el.type === "date") {
|
if (el.type === "date" || el.type === "datetime-local") {
|
||||||
const d = val instanceof Date ? val : new Date(val);
|
const d = val instanceof Date ? val : new Date(val);
|
||||||
if (!isNaN(d)) el.value = toDateInput(d);
|
if (!isNaN(d)) el.value = el.type === "date" ? toDateInput(d) : toDateTimeInput(d);
|
||||||
} else if (el.tagName === "SELECT") {
|
} else if (el.tagName === "SELECT") {
|
||||||
let opt = Array.from(el.options).find((o) => o.value === String(val));
|
let opt = Array.from(el.options).find((o) => o.value === String(val));
|
||||||
if (!opt) {
|
if (!opt) {
|
||||||
@ -517,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");
|
||||||
}
|
}
|
||||||
@ -576,8 +631,12 @@ function collectFieldValues() {
|
|||||||
}
|
}
|
||||||
if (String(raw).trim() !== "") {
|
if (String(raw).trim() !== "") {
|
||||||
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
|
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
|
||||||
// interpretiert new Date("YYYY-MM-DD") als UTC.
|
// interpretiert new Date("YYYY-MM-DD") als UTC. datetime-local ist bereits
|
||||||
const value = el.type === "date" ? new Date(`${raw}T00:00:00`) : raw;
|
// lokal und wird direkt geparst.
|
||||||
|
const value =
|
||||||
|
el.type === "date" ? new Date(`${raw}T00:00:00`)
|
||||||
|
: el.type === "datetime-local" ? new Date(raw)
|
||||||
|
: raw;
|
||||||
result[name] = { value, type };
|
result[name] = { value, type };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -643,7 +702,8 @@ async function submit() {
|
|||||||
mid === STATE.messageId ? STATE.attachments : await Mail.listAttachments(mid);
|
mid === STATE.messageId ? STATE.attachments : await Mail.listAttachments(mid);
|
||||||
|
|
||||||
// Indexfelder: manuelle Werte für alle gleich; gemappte Felder je Mail neu.
|
// Indexfelder: manuelle Werte für alle gleich; gemappte Felder je Mail neu.
|
||||||
const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi);
|
const msgCtx = { attachmentCount: atts.length };
|
||||||
|
const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi, msgCtx);
|
||||||
// Message-ID für Dedup/„bereits abgelegt" mitschreiben (wenn Schrank EML_ID hat).
|
// Message-ID für Dedup/„bereits abgelegt" mitschreiben (wenn Schrank EML_ID hat).
|
||||||
if (!STATE.isBasket && meta.rfcMessageId && fieldNames.includes("EML_ID")) {
|
if (!STATE.isBasket && meta.rfcMessageId && fieldNames.includes("EML_ID")) {
|
||||||
fields.EML_ID = { value: DocuWare.normalizeMessageId(meta.rfcMessageId), type: "Text" };
|
fields.EML_ID = { value: DocuWare.normalizeMessageId(meta.rfcMessageId), type: "Text" };
|
||||||
@ -654,14 +714,16 @@ async function submit() {
|
|||||||
await uploadOnce(`${mid}|eml`, async () => {
|
await uploadOnce(`${mid}|eml`, async () => {
|
||||||
status(`${prefix}E-Mail …`);
|
status(`${prefix}E-Mail …`);
|
||||||
const eml = await Mail.getEmlFile(mid, meta.subject);
|
const eml = await Mail.getEmlFile(mid, meta.subject);
|
||||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields);
|
const ef = applyPerFileFields(fields, meta, { ...msgCtx, fileName: eml.name });
|
||||||
|
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, ef);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (wantPdf) {
|
if (wantPdf) {
|
||||||
await uploadOnce(`${mid}|pdf`, async () => {
|
await uploadOnce(`${mid}|pdf`, async () => {
|
||||||
status(`${prefix}PDF …`);
|
status(`${prefix}PDF …`);
|
||||||
const pdf = Pdf.fromMail(meta);
|
const pdf = Pdf.fromMail(meta);
|
||||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields);
|
const pf = applyPerFileFields(fields, meta, { ...msgCtx, fileName: pdf.name });
|
||||||
|
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, pf);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (wantAtt) {
|
if (wantAtt) {
|
||||||
@ -673,7 +735,14 @@ async function submit() {
|
|||||||
await uploadOnce(`${mid}|att:${a.partName}`, async () => {
|
await uploadOnce(`${mid}|att:${a.partName}`, async () => {
|
||||||
status(`${prefix}Anhang ${a.name} …`);
|
status(`${prefix}Anhang ${a.name} …`);
|
||||||
const file = await Mail.getAttachmentFile(mid, a.partName);
|
const file = await Mail.getAttachmentFile(mid, a.partName);
|
||||||
const af = STATE.isBasket ? {} : { ...fields, DOC_FILE_NAME: { value: a.name, type: "Text" } };
|
// 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" },
|
||||||
|
};
|
||||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
|
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -720,17 +789,32 @@ async function uploadOnce(part, doUpload) {
|
|||||||
|
|
||||||
// Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch)
|
// Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch)
|
||||||
// werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben.
|
// werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben.
|
||||||
function fieldsForMessage(baseResult, meta, multi) {
|
function fieldsForMessage(baseResult, meta, multi, ctx) {
|
||||||
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
|
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
|
||||||
const result = { ...baseResult };
|
const result = { ...baseResult };
|
||||||
STATE.fields.forEach((f) => {
|
STATE.fields.forEach((f) => {
|
||||||
const val = prefillFor(f.name, meta);
|
const val = prefillFor(f.name, meta, ctx);
|
||||||
if (val === undefined || val === null || val === "") return;
|
if (val === undefined || val === null || val === "") return;
|
||||||
result[f.name] = { value: val, type: f.type };
|
result[f.name] = { value: val, type: f.type };
|
||||||
});
|
});
|
||||||
return result;
|
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.
|
// Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken.
|
||||||
async function saveOptionPrefs(mailFormat, storeAttachments) {
|
async function saveOptionPrefs(mailFormat, storeAttachments) {
|
||||||
try {
|
try {
|
||||||
@ -780,6 +864,38 @@ const toDateInput = (d) => {
|
|||||||
const day = String(d.getDate()).padStart(2, "0");
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
return `${y}-${m}-${day}`;
|
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).
|
// Sicheres CSS-Escaping für Attribut-Selektoren (Feldnamen aus DocuWare).
|
||||||
const cssEsc = (s) =>
|
const cssEsc = (s) =>
|
||||||
window.CSS && CSS.escape ? CSS.escape(String(s)) : String(s).replace(/["\\\]]/g, "\\$&");
|
window.CSS && CSS.escape ? CSS.escape(String(s)) : String(s).replace(/["\\\]]/g, "\\$&");
|
||||||
@ -811,8 +927,21 @@ $("cancel").addEventListener("click", () => window.close());
|
|||||||
|
|
||||||
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
|
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
|
||||||
document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
|
document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
|
||||||
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
|
r.addEventListener("change", () => {
|
||||||
|
refreshPerFilePreview(); // .eml ↔ .pdf: Dateiname-Vorschau nachziehen
|
||||||
|
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", () => {
|
$("optAtt").addEventListener("change", () => {
|
||||||
const show = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0;
|
const show = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0;
|
||||||
$("attList").style.display = show ? "" : "none";
|
$("attList").style.display = show ? "" : "none";
|
||||||
|
|||||||
@ -427,8 +427,48 @@ 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
|
||||||
|
// 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")) {
|
if (t.includes("date")) {
|
||||||
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
|
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
|
||||||
const d = value instanceof Date ? value : new Date(value);
|
const d = value instanceof Date ? value : new Date(value);
|
||||||
@ -446,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) {
|
||||||
|
|||||||
19
lib/mail.js
19
lib/mail.js
@ -16,9 +16,11 @@ const Mail = {
|
|||||||
const bodyText = this._extractText(full);
|
const bodyText = this._extractText(full);
|
||||||
|
|
||||||
const sender = this._parseAddress(header.author);
|
const sender = this._parseAddress(header.author);
|
||||||
const account = await this._accountForMessage(messageId);
|
const acc = await this._accountInfo(messageId);
|
||||||
|
|
||||||
return {
|
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)
|
rfcMessageId: this._header(full, "message-id"), // RFC822 Message-ID (Dedup-Schlüssel)
|
||||||
subject: header.subject || "",
|
subject: header.subject || "",
|
||||||
senderEmail: sender.email,
|
senderEmail: sender.email,
|
||||||
@ -28,7 +30,7 @@ const Mail = {
|
|||||||
bcc: (header.bccList || []).join(", "),
|
bcc: (header.bccList || []).join(", "),
|
||||||
date: header.date instanceof Date ? header.date : new Date(header.date),
|
date: header.date instanceof Date ? header.date : new Date(header.date),
|
||||||
bodyText,
|
bodyText,
|
||||||
account,
|
account: acc.name,
|
||||||
direction: this._direction(header),
|
direction: this._direction(header),
|
||||||
sizeBytes: header.size || 0,
|
sizeBytes: header.size || 0,
|
||||||
};
|
};
|
||||||
@ -128,16 +130,23 @@ const Mail = {
|
|||||||
return { name: "", email: str.trim() };
|
return { name: "", email: str.trim() };
|
||||||
},
|
},
|
||||||
|
|
||||||
async _accountForMessage(messageId) {
|
/**
|
||||||
|
* Konto-Name und eigene E-Mail-Adresse (erste Identität) zur Nachricht.
|
||||||
|
* @returns {Promise<{name:string, email:string}>}
|
||||||
|
*/
|
||||||
|
async _accountInfo(messageId) {
|
||||||
try {
|
try {
|
||||||
const msg = await browser.messages.get(messageId);
|
const msg = await browser.messages.get(messageId);
|
||||||
const folder = msg.folder;
|
const folder = msg.folder;
|
||||||
if (folder && folder.accountId) {
|
if (folder && folder.accountId) {
|
||||||
const acc = await browser.accounts.get(folder.accountId);
|
const acc = await browser.accounts.get(folder.accountId);
|
||||||
return acc ? acc.name : "";
|
if (acc) {
|
||||||
|
const ident = (acc.identities || [])[0];
|
||||||
|
return { name: acc.name || "", email: (ident && ident.email) || "" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return "";
|
return { name: "", email: "" };
|
||||||
},
|
},
|
||||||
|
|
||||||
_direction(header) {
|
_direction(header) {
|
||||||
|
|||||||
22
lib/store.js
22
lib/store.js
@ -32,6 +32,27 @@ const MAIL_TOKENS = [
|
|||||||
{ key: "direction", label: "Richtung (Ein-/Ausgang)" },
|
{ key: "direction", label: "Richtung (Ein-/Ausgang)" },
|
||||||
{ key: "size", label: "Größe (Bytes)" },
|
{ key: "size", label: "Größe (Bytes)" },
|
||||||
{ key: "account", label: "Konto" },
|
{ 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
|
// Standard-Zuordnung DocuWare-Feldname -> Mail-Token. Greift, solange der Nutzer
|
||||||
@ -53,6 +74,7 @@ const DEFAULT_FIELD_MAP = {
|
|||||||
EML_SIZE: "size",
|
EML_SIZE: "size",
|
||||||
EML_ACCOUNT: "account",
|
EML_ACCOUNT: "account",
|
||||||
DOC_DATE: "date",
|
DOC_DATE: "date",
|
||||||
|
DOC_FILE_NAME: "fileName", // .eml/PDF: Mailname, Anhang: eigener Dateiname
|
||||||
SUBJECT: "subject",
|
SUBJECT: "subject",
|
||||||
BETREFF: "subject",
|
BETREFF: "subject",
|
||||||
EMAIL: "senderEmail",
|
EMAIL: "senderEmail",
|
||||||
|
|||||||
@ -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.11.0",
|
"version": "0.12.3",
|
||||||
"author": "l.lingler",
|
"author": "l.lingler",
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
16
updates.json
16
updates.json
@ -41,6 +41,22 @@
|
|||||||
{
|
{
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.11.0/docuware-ablage-0.11.0.xpi"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user