feat(diarization-ui): add multi-select bulk actions to library view

Add checkboxes to each row, select-all/deselect-all controls, and a
bulk action bar for moving or deleting multiple documents at once.
New endpoints: POST /documents/bulk-move and /documents/bulk-delete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 09:40:27 +01:00
parent 0fab574b68
commit 239875c7f6

94
app.py
View File

@@ -5,7 +5,7 @@ import threading
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from pathlib import Path
from typing import Optional
from typing import List, Optional
import markdown as md
import requests
@@ -561,6 +561,7 @@ def library(project_id: Optional[str] = None, q_title: str = "", q_content: str
rows = "".join(
[
f"<tr>"
f"<td class='cb-cell'><input type='checkbox' class='row-cb' value='{d['id']}'></td>"
f"<td>#{d['id']}</td>"
f"<td>{d['title']}</td>"
f"<td>{d['kind']}</td>"
@@ -587,6 +588,10 @@ def library(project_id: Optional[str] = None, q_title: str = "", q_content: str
.grid th,.grid td{{padding:10px 12px;border-bottom:1px solid #1f2937;text-align:left}}
.grid th{{position:sticky;top:0;background:#0b1222;color:#94a3b8;font-size:12px;text-transform:uppercase;letter-spacing:.04em}}
.grid tr:hover{{background:rgba(56,189,248,.06)}}
.grid tr.selected{{background:rgba(56,189,248,.12)}}
.cb-cell{{width:32px;padding:10px 4px 10px 12px}}
.bulk-bar{{display:none;gap:8px;align-items:center;padding:8px 12px;background:rgba(56,189,248,.08);border-radius:6px;margin-bottom:8px}}
.bulk-bar.active{{display:flex}}
</style>
<h2 style='margin-bottom:6px'>Projekte · Dokumente</h2>
<div class='hint' style='margin-bottom:10px'>Ansicht im Projektlisten-Stil mit Schnellaktionen.</div>
@@ -599,18 +604,72 @@ def library(project_id: Optional[str] = None, q_title: str = "", q_content: str
<div class='spacer'></div>
<small>{len(docs)} Treffer</small>
</form>
<div class='bulk-bar' id='bulkBar'>
<strong id='bulkCount'>0 ausgewählt</strong>
<button type='button' onclick='bulkMove()'>📁 Verschieben</button>
<button type='button' onclick='bulkDelete()' style='color:#f87171'>🗑️ Löschen</button>
</div>
<div class='card gridcard'>
<table class='grid'>
<thead><tr><th>ID</th><th>Titel</th><th>Typ</th><th>Projekt</th><th>Erstellt</th><th>Aktionen</th></tr></thead>
<tbody>{rows or "<tr><td colspan='6'>Keine Einträge.</td></tr>"}</tbody>
<thead><tr>
<th class='cb-cell'><input type='checkbox' id='cbAll' title='Alle wählen'></th>
<th>ID</th><th>Titel</th><th>Typ</th><th>Projekt</th><th>Erstellt</th><th>Aktionen</th>
</tr></thead>
<tbody>{rows or "<tr><td colspan='7'>Keine Einträge.</td></tr>"}</tbody>
</table>
</div>
<div style='display:flex;gap:8px;margin-top:8px'>
<button type='button' onclick='selectAll()'>Alle wählen</button>
<button type='button' onclick='selectNone()'>Alle abwählen</button>
</div>
<script>
async function libPost(url, data) {{
const r = await fetch(url, {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body:new URLSearchParams(data)}});
if(!r.ok) {{ alert('Fehler '+r.status); return; }}
location.reload();
}}
async function libPostMulti(url, params) {{
const body = new URLSearchParams();
for(const [k,v] of Object.entries(params)) {{
if(Array.isArray(v)) v.forEach(x => body.append(k, x));
else body.append(k, v);
}}
const r = await fetch(url, {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body}});
if(!r.ok) {{ alert('Fehler '+r.status); return; }}
location.reload();
}}
function getSelected() {{
return [...document.querySelectorAll('.row-cb:checked')].map(cb => cb.value);
}}
function updateBulkBar() {{
const ids = getSelected();
const bar = document.getElementById('bulkBar');
document.getElementById('bulkCount').textContent = ids.length + ' ausgewählt';
bar.classList.toggle('active', ids.length > 0);
document.getElementById('cbAll').indeterminate = ids.length > 0 && ids.length < document.querySelectorAll('.row-cb').length;
document.getElementById('cbAll').checked = ids.length > 0 && ids.length === document.querySelectorAll('.row-cb').length;
}}
document.querySelectorAll('.row-cb').forEach(cb => {{
cb.addEventListener('change', function() {{
this.closest('tr').classList.toggle('selected', this.checked);
updateBulkBar();
}});
}});
document.getElementById('cbAll').addEventListener('change', function() {{
document.querySelectorAll('.row-cb').forEach(cb => {{
cb.checked = this.checked;
cb.closest('tr').classList.toggle('selected', this.checked);
}});
updateBulkBar();
}});
function selectAll() {{
document.querySelectorAll('.row-cb').forEach(cb => {{ cb.checked=true; cb.closest('tr').classList.add('selected'); }});
updateBulkBar();
}}
function selectNone() {{
document.querySelectorAll('.row-cb').forEach(cb => {{ cb.checked=false; cb.closest('tr').classList.remove('selected'); }});
updateBulkBar();
}}
window.libRename = async function(id, current) {{
const v = await window.uiPrompt('Dokument umbenennen', current || '');
if(v===null) return;
@@ -627,6 +686,21 @@ window.libDelete = async function(id) {{
if(!ok) return;
await libPost(`/document/${{id}}/delete`, {{}});
}};
window.bulkMove = async function() {{
const ids = getSelected();
if(!ids.length) return;
const options = {project_js};
const v = await window.uiSelect(`${{ids.length}} Dokumente verschieben`, options, 'Projekt wählen');
if(v===null || v==='') return;
await libPostMulti('/documents/bulk-move', {{ids, project_id:v}});
}};
window.bulkDelete = async function() {{
const ids = getSelected();
if(!ids.length) return;
const ok = await window.uiConfirm(`${{ids.length}} Dokumente löschen?`);
if(!ok) return;
await libPostMulti('/documents/bulk-delete', {{ids}});
}};
</script>
"""
return layout("Library", body)
@@ -730,6 +804,20 @@ def delete_document(doc_id: int):
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
@app.post("/documents/bulk-move", response_class=HTMLResponse)
def bulk_move_documents(ids: List[int] = Form(...), project_id: int = Form(...)):
with db() as c:
c.executemany("UPDATE documents SET project_id=? WHERE id=?", [(project_id, i) for i in ids])
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
@app.post("/documents/bulk-delete", response_class=HTMLResponse)
def bulk_delete_documents(ids: List[int] = Form(...)):
with db() as c:
c.executemany("DELETE FROM documents WHERE id=?", [(i,) for i in ids])
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
@app.get("/prompts", response_class=HTMLResponse)
def prompts_page():
with db() as c: