feat(ui): Sammelaktionen auch in der klassischen Ansicht, mit Lucide-Icons
Im Studio lassen sich mehrere Faelle auf einmal archivieren oder loeschen, in der klassischen Ansicht ging das nur einzeln ueber die Aktionsleiste. Wer dort zehn alte Lagen wegraeumen wollte, musste jede einzeln oeffnen. Die Seitenleiste bekommt deshalb dieselbe Bedienung wie das Studio. Unten stehen zwei Knoepfe, Archivieren und Loeschen. Der erste Klick schaltet den Auswahlmodus ein, erst dann erscheinen Haken in der Lagenliste, an Stelle des Statuspunktes. Ein Klick auf eine Zeile waehlt dann aus, statt die Lage zu oeffnen. Bestaetigt wird in der Leiste darunter, die mitzaehlt und nur anbietet, was zur Auswahl passt. Sind nur aktive Lagen gewaehlt, erscheint kein Aktivieren-Knopf, im Loeschmodus keiner der beiden anderen. "Alle sichtbaren" beruecksichtigt den Filter der Seitenleiste, waehlt also bei aktivem Eigene-Filter nur die eigenen Lagen aus. Das Loeschen fragt vorher nach und nennt Titel und Artikelzahl, weil es nicht rueckgaengig zu machen ist. Die Aktionen laufen nacheinander ueber die normalen Endpunkte, damit deren Rechtepruefung greift, und Fehlschlaege werden mit Nummer gemeldet statt still verschluckt. War die offene Lage dabei, springt die Ansicht zurueck. Dazu Lucide-Icons fuer Archivieren, Aktivieren, Loeschen und Abbrechen, in beiden Ansichten. Im Studio setzte _renderBulkBar die Beschriftung bisher ueber textContent des Knopfes, was das Icon bei jedem Neuzeichnen entfernt haette. Die Beschriftung liegt jetzt in einer eigenen span. Cache-Buster auf 20260802a, sonst laden Browser die alten Skripte weiter. Neu sind 34 Pruefungen, sie laufen ohne Browser gegen ein DOM-Double. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -544,6 +544,150 @@ const App = {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// === Sammelaktionen in der Seitenleiste ===
|
||||
// Die Haken sind normalerweise weg. Erst ein Klick auf Archivieren oder
|
||||
// Loeschen unten in der Seitenleiste schaltet den Auswahlmodus ein. Das
|
||||
// entspricht dem Verhalten im Studio, damit beide Ansichten sich gleich
|
||||
// bedienen lassen.
|
||||
_bulkMode: null,
|
||||
_bulkSel: new Set(),
|
||||
|
||||
startBulkMode(mode) {
|
||||
this._bulkMode = mode;
|
||||
this._bulkSel.clear();
|
||||
this.renderSidebar();
|
||||
},
|
||||
|
||||
endBulkMode() {
|
||||
this._bulkMode = null;
|
||||
this._bulkSel.clear();
|
||||
const alle = document.getElementById('sidebar-bulk-all');
|
||||
if (alle) alle.checked = false;
|
||||
this.renderSidebar();
|
||||
},
|
||||
|
||||
toggleBulkSel(id, on) {
|
||||
if (on) this._bulkSel.add(id); else this._bulkSel.delete(id);
|
||||
this.renderSidebar();
|
||||
},
|
||||
|
||||
// Sichtbar heisst: was der aktive Filter der Seitenleiste gerade zeigt.
|
||||
_visibleIncidents() {
|
||||
let sichtbar = this.incidents || [];
|
||||
if (this._sidebarFilter === 'mine') {
|
||||
sichtbar = sichtbar.filter(i => i.created_by_username === this._currentUsername);
|
||||
}
|
||||
return sichtbar;
|
||||
},
|
||||
|
||||
selectAllVisible(on) {
|
||||
const sichtbar = this._visibleIncidents();
|
||||
if (on) sichtbar.forEach(i => this._bulkSel.add(i.id));
|
||||
else sichtbar.forEach(i => this._bulkSel.delete(i.id));
|
||||
this.renderSidebar();
|
||||
},
|
||||
|
||||
_renderBulkBar() {
|
||||
const mode = this._bulkMode;
|
||||
const tools = document.getElementById('sidebar-bulk-tools');
|
||||
const bar = document.getElementById('sidebar-bulk-bar');
|
||||
if (tools) tools.hidden = mode !== null;
|
||||
if (bar) bar.hidden = mode === null;
|
||||
if (mode === null) return;
|
||||
|
||||
const gewaehlt = (this.incidents || []).filter(i => this._bulkSel.has(i.id));
|
||||
const n = gewaehlt.length;
|
||||
const aktive = gewaehlt.filter(i => i.status === 'active').length;
|
||||
const archivierte = n - aktive;
|
||||
|
||||
const zaehler = document.getElementById('sidebar-bulk-count');
|
||||
if (zaehler) {
|
||||
zaehler.textContent = n === 0
|
||||
? (mode === 'delete' ? 'Lagen zum Löschen anhaken' : 'Lagen zum Archivieren anhaken')
|
||||
: (n === 1 ? '1 Lage ausgewählt' : n + ' Lagen ausgewählt');
|
||||
}
|
||||
|
||||
// Nur anbieten, was zum Modus und zur Auswahl passt.
|
||||
const knopfArchiv = document.getElementById('sidebar-bulk-archive');
|
||||
const knopfAktiv = document.getElementById('sidebar-bulk-activate');
|
||||
const knopfLoesch = document.getElementById('sidebar-bulk-delete');
|
||||
if (knopfArchiv) {
|
||||
knopfArchiv.hidden = mode !== 'archive' || aktive === 0;
|
||||
const t = knopfArchiv.querySelector('span');
|
||||
if (t) t.textContent = 'Archivieren (' + aktive + ')';
|
||||
}
|
||||
if (knopfAktiv) {
|
||||
knopfAktiv.hidden = mode !== 'archive' || archivierte === 0;
|
||||
const t = knopfAktiv.querySelector('span');
|
||||
if (t) t.textContent = 'Aktivieren (' + archivierte + ')';
|
||||
}
|
||||
if (knopfLoesch) {
|
||||
knopfLoesch.hidden = mode !== 'delete' || n === 0;
|
||||
const t = knopfLoesch.querySelector('span');
|
||||
if (t) t.textContent = 'Löschen (' + n + ')';
|
||||
}
|
||||
|
||||
const sichtbar = this._visibleIncidents();
|
||||
const alle = document.getElementById('sidebar-bulk-all');
|
||||
if (alle) alle.checked = sichtbar.length > 0 && sichtbar.every(i => this._bulkSel.has(i.id));
|
||||
},
|
||||
|
||||
// Die Sammelaktionen laufen nacheinander ueber die normalen Endpunkte, damit
|
||||
// deren Rechtepruefung greift. Fehlschlaege werden gezaehlt statt still
|
||||
// verschluckt.
|
||||
async _bulkRun(ids, fn, verb) {
|
||||
let ok = 0;
|
||||
const fehlgeschlagen = [];
|
||||
for (const id of ids) {
|
||||
try { await fn(id); ok++; }
|
||||
catch (e) { fehlgeschlagen.push(id); }
|
||||
}
|
||||
this._bulkSel.clear();
|
||||
this._bulkMode = null;
|
||||
await this.loadIncidents();
|
||||
if (fehlgeschlagen.length) {
|
||||
UI.showToast(ok + ' ' + verb + ', ' + fehlgeschlagen.length
|
||||
+ ' fehlgeschlagen (IDs ' + fehlgeschlagen.join(', ') + ')', 'error');
|
||||
} else {
|
||||
UI.showToast(ok + ' ' + (ok === 1 ? 'Lage' : 'Lagen') + ' ' + verb, 'success');
|
||||
}
|
||||
},
|
||||
|
||||
async bulkStatus(status) {
|
||||
const ids = (this.incidents || [])
|
||||
.filter(i => this._bulkSel.has(i.id)
|
||||
&& (status === 'archived' ? i.status === 'active' : i.status !== 'active'))
|
||||
.map(i => i.id);
|
||||
if (!ids.length) return;
|
||||
const verb = status === 'archived' ? 'archiviert' : 'wieder aktiviert';
|
||||
await this._bulkRun(ids, (id) => API.updateIncident(id, { status }), verb);
|
||||
},
|
||||
|
||||
async bulkDelete() {
|
||||
const gewaehlt = (this.incidents || []).filter(i => this._bulkSel.has(i.id));
|
||||
if (!gewaehlt.length) return;
|
||||
const artikel = gewaehlt.reduce((n, i) => n + (i.article_count || 0), 0);
|
||||
const namen = gewaehlt.slice(0, 8).map(i => '\n\u2022 ' + i.title).join('')
|
||||
+ (gewaehlt.length > 8 ? '\n\u2022 … und ' + (gewaehlt.length - 8) + ' weitere' : '');
|
||||
const frage = gewaehlt.length + ' ' + (gewaehlt.length === 1 ? 'Lage' : 'Lagen')
|
||||
+ ' endgültig löschen?' + namen
|
||||
+ '\n\nDamit gehen ' + artikel + ' Artikel samt Faktenchecks, Lageberichten'
|
||||
+ ' und Karten verloren. Das lässt sich nicht rückgängig machen.'
|
||||
+ '\n\nWenn du sie nur aus der Liste nehmen willst, nutze stattdessen Archivieren.';
|
||||
if (!confirm(frage)) return;
|
||||
|
||||
const ids = gewaehlt.map(i => i.id);
|
||||
const offeneDabei = this.currentIncidentId && ids.includes(this.currentIncidentId);
|
||||
await this._bulkRun(ids, (id) => API.deleteIncident(id), 'gelöscht');
|
||||
if (offeneDabei) {
|
||||
this.currentIncidentId = null;
|
||||
const leer = document.getElementById('empty-state');
|
||||
const inhalt = document.getElementById('incident-view');
|
||||
if (leer) leer.style.display = 'flex';
|
||||
if (inhalt) inhalt.style.display = 'none';
|
||||
}
|
||||
},
|
||||
renderSidebar() {
|
||||
const activeContainer = document.getElementById('active-incidents');
|
||||
const researchContainer = document.getElementById('active-research');
|
||||
@@ -597,6 +741,9 @@ const App = {
|
||||
|
||||
// Sidebar-Stats aktualisieren
|
||||
this.updateSidebarStats();
|
||||
|
||||
// Zustand der Sammelaktionen nachziehen
|
||||
this._renderBulkBar();
|
||||
},
|
||||
|
||||
setSidebarFilter(filter) {
|
||||
|
||||
@@ -59,9 +59,20 @@ const UI = {
|
||||
}
|
||||
}
|
||||
|
||||
// Im Auswahlmodus der Seitenleiste tritt ein Haken an die Stelle des
|
||||
// Statuspunktes und ein Klick waehlt aus, statt die Lage zu oeffnen.
|
||||
const auswahl = App._bulkMode != null;
|
||||
const gewaehlt = auswahl && App._bulkSel.has(incident.id);
|
||||
const klick = auswahl
|
||||
? `App.toggleBulkSel(${incident.id}, !App._bulkSel.has(${incident.id}))`
|
||||
: `App.selectIncident(${incident.id})`;
|
||||
const markierung = auswahl
|
||||
? `<label class="incident-check" title="Auswählen" onclick="event.stopPropagation()"><input type="checkbox" ${gewaehlt ? 'checked' : ''} onchange="App.toggleBulkSel(${incident.id}, this.checked)"></label>`
|
||||
: `<span class="incident-dot ${dotClass}" id="dot-${incident.id}" aria-hidden="true"></span>`;
|
||||
|
||||
return `
|
||||
<div class="incident-item ${activeClass}${refreshClass}" data-id="${incident.id}" onclick="App.selectIncident(${incident.id})" role="button" tabindex="0">
|
||||
<span class="incident-dot ${dotClass}" id="dot-${incident.id}" aria-hidden="true"></span>
|
||||
<div class="incident-item ${activeClass}${refreshClass}${gewaehlt ? ' picked' : ''}" data-id="${incident.id}" onclick="${klick}" role="button" tabindex="0">
|
||||
${markierung}
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="incident-name">${this.escape(incident.title)}</div>
|
||||
<div class="incident-meta">${incident.article_count} Artikel · ${this.escape(creator)}${incident.ai_backend === 'bedrock' ? ' · <span class="sidebar-ai-tag ai-eu">EU</span>' : (incident.ai_backend === 'cli' ? ' · <span class="sidebar-ai-tag ai-anthropic">Anthropic</span>' : '')}</div>
|
||||
|
||||
@@ -459,9 +459,15 @@ const Studio = {
|
||||
const ba = document.getElementById('bulk-archive');
|
||||
const bv = document.getElementById('bulk-activate');
|
||||
const bd = document.getElementById('bulk-delete');
|
||||
if (ba) { ba.hidden = mode !== 'archive' || !anyActive; ba.textContent = `Archivieren (${sel.filter(i => i.status === 'active').length})`; }
|
||||
if (bv) { bv.hidden = mode !== 'archive' || !anyArchived; bv.textContent = `Aktivieren (${sel.filter(i => i.status !== 'active').length})`; }
|
||||
if (bd) { bd.hidden = mode !== 'delete' || n === 0; bd.textContent = `Löschen (${n})`; }
|
||||
// Beschriftung in die span, sonst wuerde textContent das Icon entfernen.
|
||||
const setzeText = (el, text) => {
|
||||
if (!el) return;
|
||||
const ziel = el.querySelector('span');
|
||||
if (ziel) ziel.textContent = text; else el.textContent = text;
|
||||
};
|
||||
if (ba) { ba.hidden = mode !== 'archive' || !anyActive; setzeText(ba, `Archivieren (${sel.filter(i => i.status === 'active').length})`); }
|
||||
if (bv) { bv.hidden = mode !== 'archive' || !anyArchived; setzeText(bv, `Aktivieren (${sel.filter(i => i.status !== 'active').length})`); }
|
||||
if (bd) { bd.hidden = mode !== 'delete' || n === 0; setzeText(bd, `Löschen (${n})`); }
|
||||
|
||||
// "Alle"-Haken spiegelt, ob die sichtbare Menge komplett gewählt ist
|
||||
const vis = this._visibleCases();
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren