Anpassungen für ATN-Listing
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-i18n="classifyTitle">Klassifizierung bearbeiten</title>
|
||||
<link rel="stylesheet" href="gemeinsam.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1 data-i18n="classifyTitle">Klassifizierung bearbeiten</h1>
|
||||
<div id="quelle" class="hinweis"></div>
|
||||
|
||||
<label for="titel" data-i18n="fieldTitle">Titel</label>
|
||||
<input type="text" id="titel">
|
||||
|
||||
<label for="erstellt" data-i18n="fieldCreated">Erstellt</label>
|
||||
<input type="text" id="erstellt" placeholder="JJJJ-MM-TT">
|
||||
|
||||
<label for="korr" data-i18n="fieldCorrespondent">Korrespondent</label>
|
||||
<input type="text" id="korr" list="korrListe" autocomplete="off">
|
||||
<datalist id="korrListe"></datalist>
|
||||
|
||||
<label for="typ" data-i18n="fieldDocType">Dokumenttyp</label>
|
||||
<input type="text" id="typ" list="typListe" autocomplete="off">
|
||||
<datalist id="typListe"></datalist>
|
||||
|
||||
<div class="wachsen">
|
||||
<label for="tagSuche" data-i18n="fieldTags">Tags</label>
|
||||
<input type="text" id="tagSuche"
|
||||
data-i18n-placeholder="tagsFilterPlaceholder"
|
||||
placeholder="Tippen zum Filtern …">
|
||||
<div class="liste">
|
||||
<div id="tagsGewaehlt" class="gewaehlteTags"></div>
|
||||
<div id="tags" style="padding:4px 8px"></div>
|
||||
<div id="tagsLeer" class="hinweis"
|
||||
style="padding:6px 8px; display:none"
|
||||
data-i18n="tagsNoMatch">Keine Übereinstimmung.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hinweis" data-i18n="classifyHintNew">
|
||||
Unbekannte Korrespondenten und Dokumenttypen werden angelegt.
|
||||
</div>
|
||||
|
||||
<div id="meldung" class="hinweis"></div>
|
||||
|
||||
<div class="fuss">
|
||||
<button id="abbrechenAlle" data-i18n="btnSkipAll">Alle überspringen</button>
|
||||
<button id="ueberspringen" data-i18n="btnSkip">Überspringen</button>
|
||||
<button id="speichern" class="haupt" data-i18n="btnSave">Speichern</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="classify.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
262
ui/classify.js
262
ui/classify.js
@@ -1,262 +0,0 @@
|
||||
/**
|
||||
* classify.js - Klassifizierung eines gerade abgelegten Dokuments.
|
||||
*
|
||||
* Wird unmittelbar nach dem Ablegen aufgerufen. Ohne diesen Schritt trägt
|
||||
* ein neues Dokument nur den Titel, und nachträglich macht man es
|
||||
* erfahrungsgemäß nicht mehr.
|
||||
*/
|
||||
|
||||
import { kurz } from "../lib/settings.js";
|
||||
import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen }
|
||||
from "./gemeinsam.js";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let dokumentId = null;
|
||||
let alleTags = [];
|
||||
|
||||
// Bei mehreren abgelegten Dateien wird jede einzeln klassifiziert.
|
||||
// Andernfalls trüge nur die erste eine sinnvolle Zuordnung, und die
|
||||
// übrigen fielen später als unklassifiziert auf - wenn überhaupt.
|
||||
let liste = [];
|
||||
let stelle = 0;
|
||||
|
||||
// Gewählte Tags als Menge. Bewusst getrennt vom angezeigten Zustand der
|
||||
// Kontrollkästchen: Der Filter blendet Zeilen aus, die Auswahl darf davon
|
||||
// nicht berührt werden.
|
||||
const gewaehlt = new Set();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", start);
|
||||
|
||||
async function start() {
|
||||
uebersetzeSeite();
|
||||
const p = new URLSearchParams(location.search);
|
||||
|
||||
// "ids" für mehrere, "id" für den Einzelfall - letzteres bleibt für
|
||||
// Aufrufe aus anderen Stellen erhalten.
|
||||
const roh = p.get("ids") || p.get("id") || "";
|
||||
liste = roh.split(",").map((x) => Number(x)).filter(Boolean);
|
||||
stelle = 0;
|
||||
|
||||
$("ueberspringen").addEventListener("click", weiter);
|
||||
$("abbrechenAlle").addEventListener("click", () => window.close());
|
||||
$("speichern").addEventListener("click", speichern);
|
||||
|
||||
if (!liste.length) {
|
||||
fehlerAnzeigen("Kein Dokument angegeben.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stamm = await ruf("stammdaten");
|
||||
alleTags = stamm.tags;
|
||||
fuelleListe("korrListe", stamm.korr);
|
||||
fuelleListe("typListe", stamm.typen);
|
||||
zustand.korr = stamm.korr;
|
||||
zustand.typen = stamm.typen;
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
await anzeigen();
|
||||
}
|
||||
|
||||
const zustand = { korr: [], typen: [] };
|
||||
|
||||
/** Das Dokument an der aktuellen Stelle laden und darstellen. */
|
||||
async function anzeigen() {
|
||||
dokumentId = liste[stelle];
|
||||
$("speichern").disabled = false;
|
||||
$("ueberspringen").disabled = false;
|
||||
meldungAnzeigen("");
|
||||
|
||||
// Der Knopf zum Abbrechen aller weiteren ist nur sinnvoll, wenn noch
|
||||
// welche folgen.
|
||||
$("abbrechenAlle").hidden = liste.length < 2;
|
||||
|
||||
try {
|
||||
const doc = await ruf("dokument", { id: dokumentId });
|
||||
|
||||
const zaehler = liste.length > 1
|
||||
? ` · ${browser.i18n.getMessage("classifyProgress",
|
||||
[String(stelle + 1), String(liste.length)])
|
||||
|| `${stelle + 1} von ${liste.length}`}`
|
||||
: "";
|
||||
$("quelle").textContent = `#${dokumentId} · ${kurz(doc.title, 60)}`
|
||||
+ zaehler;
|
||||
|
||||
$("titel").value = doc.title || "";
|
||||
$("erstellt").value = (doc.created || "").slice(0, 10);
|
||||
$("korr").value = nameVon(zustand.korr, doc.correspondent);
|
||||
$("typ").value = nameVon(zustand.typen, doc.document_type);
|
||||
zeigeTags(doc.tags || []);
|
||||
if ($("tagSuche")) $("tagSuche").value = "";
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Zum nächsten Dokument, oder schliessen. */
|
||||
async function weiter() {
|
||||
stelle += 1;
|
||||
if (stelle >= liste.length) {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
await anzeigen();
|
||||
}
|
||||
|
||||
function fuelleListe(id, eintraege) {
|
||||
const dl = $(id);
|
||||
dl.replaceChildren();
|
||||
for (const e of eintraege) {
|
||||
const o = document.createElement("option");
|
||||
o.value = e.name;
|
||||
dl.append(o);
|
||||
}
|
||||
}
|
||||
|
||||
function nameVon(liste, id) {
|
||||
return liste.find((x) => x.id === id)?.name || "";
|
||||
}
|
||||
|
||||
function zeigeTags(gesetzt) {
|
||||
for (const id of gesetzt) gewaehlt.add(Number(id));
|
||||
const feld = $("tagSuche");
|
||||
if (feld) {
|
||||
feld.addEventListener("input", zeichneTags);
|
||||
// Eingabetaste soll den Dialog nicht abschicken, sondern beim
|
||||
// einzigen Treffer diesen umschalten - das ist beim Tippen der
|
||||
// erwartete Ablauf.
|
||||
feld.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
e.preventDefault();
|
||||
const treffer = gefilterteTags();
|
||||
if (treffer.length === 1) {
|
||||
umschalten(treffer[0].id);
|
||||
feld.value = "";
|
||||
zeichneTags();
|
||||
}
|
||||
});
|
||||
}
|
||||
zeichneTags();
|
||||
}
|
||||
|
||||
/** Tags, die zum aktuellen Filter passen und noch nicht gewählt sind. */
|
||||
function gefilterteTags() {
|
||||
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
|
||||
return alleTags.filter((t) => {
|
||||
if (gewaehlt.has(Number(t.id))) return false;
|
||||
if (!filter) return true;
|
||||
return String(t.name).toLowerCase().includes(filter);
|
||||
});
|
||||
}
|
||||
|
||||
function umschalten(id) {
|
||||
const n = Number(id);
|
||||
if (gewaehlt.has(n)) gewaehlt.delete(n);
|
||||
else gewaehlt.add(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tagliste zeichnen.
|
||||
*
|
||||
* Zwei Bereiche: oben die gewählten, darunter die übrigen. Die gewählten
|
||||
* bleiben immer sichtbar, auch wenn der Filter sie ausschliesst - sonst
|
||||
* verliert man beim Suchen den Überblick, was bereits angehakt ist.
|
||||
*/
|
||||
function zeichneTags() {
|
||||
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
|
||||
|
||||
const oben = $("tagsGewaehlt");
|
||||
oben.replaceChildren();
|
||||
const gewaehlteListe = alleTags
|
||||
.filter((t) => gewaehlt.has(Number(t.id)))
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
|
||||
for (const tag of gewaehlteListe) {
|
||||
oben.append(tagZeile(tag, true, ""));
|
||||
}
|
||||
|
||||
const unten = $("tags");
|
||||
unten.replaceChildren();
|
||||
const rest = gefilterteTags()
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
|
||||
for (const tag of rest) {
|
||||
unten.append(tagZeile(tag, false, filter));
|
||||
}
|
||||
|
||||
const leer = $("tagsLeer");
|
||||
if (leer) leer.style.display = (rest.length === 0 && filter) ? "" : "none";
|
||||
}
|
||||
|
||||
function tagZeile(tag, istGewaehlt, filter) {
|
||||
const zeile = document.createElement("div");
|
||||
zeile.className = "wahl";
|
||||
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.id = `t${tag.id}`;
|
||||
box.value = String(tag.id);
|
||||
box.checked = istGewaehlt;
|
||||
box.addEventListener("change", () => {
|
||||
umschalten(tag.id);
|
||||
zeichneTags();
|
||||
});
|
||||
|
||||
const bez = document.createElement("label");
|
||||
bez.htmlFor = box.id;
|
||||
beschriftungSetzen(bez, String(tag.name), filter);
|
||||
|
||||
zeile.append(box, bez);
|
||||
return zeile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beschriftung mit hervorgehobenem Treffer.
|
||||
*
|
||||
* Aufbau über createElement und textContent, nicht über innerHTML - ein
|
||||
* Tagname stammt aus Paperless und damit mittelbar aus fremden Quellen.
|
||||
*/
|
||||
function beschriftungSetzen(el, name, filter) {
|
||||
el.replaceChildren();
|
||||
if (!filter) {
|
||||
el.textContent = name;
|
||||
return;
|
||||
}
|
||||
const pos = name.toLowerCase().indexOf(filter);
|
||||
if (pos < 0) {
|
||||
el.textContent = name;
|
||||
return;
|
||||
}
|
||||
el.append(document.createTextNode(name.slice(0, pos)));
|
||||
const treffer = document.createElement("span");
|
||||
treffer.className = "trefferHervor";
|
||||
treffer.textContent = name.slice(pos, pos + filter.length);
|
||||
el.append(treffer);
|
||||
el.append(document.createTextNode(name.slice(pos + filter.length)));
|
||||
}
|
||||
|
||||
async function speichern() {
|
||||
$("speichern").disabled = true;
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …");
|
||||
try {
|
||||
// Aus der Menge lesen, nicht aus den Kästchen: Gefilterte Zeilen
|
||||
// sind gar nicht im Dokument, ihre Auswahl ginge sonst verloren.
|
||||
const tags = [...gewaehlt];
|
||||
const payload = { title: $("titel").value.trim(), tags };
|
||||
const erstellt = $("erstellt").value.trim();
|
||||
if (erstellt) payload.created = erstellt;
|
||||
|
||||
await ruf("klassifizieren", {
|
||||
id: dokumentId,
|
||||
payload,
|
||||
korrName: $("korr").value.trim(),
|
||||
typName: $("typ").value.trim()
|
||||
});
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgSaved") || "Gespeichert.");
|
||||
setTimeout(weiter, 600);
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
$("speichern").disabled = false;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<div class="wachsen">
|
||||
<label data-i18n="colVersions">Versionen</label>
|
||||
<div class="liste" style="min-height:70px">
|
||||
<div class="liste" >
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class="zahl" data-i18n="colNo">Nr</th>
|
||||
@@ -68,6 +68,7 @@
|
||||
</div>
|
||||
<div class="hinweis" data-i18n="insertHintLink">
|
||||
Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt.
|
||||
Leeres Tag-Feld bedeutet: keine Kennzeichnung setzen.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user