@ -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 ( rowFieldNam e) ;
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 = row FieldName( 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 ) {