feat(diarization-ui): re-add bulk-select to library after Bootstrap merge

Restore multi-row checkbox selection, select-all/deselect-all controls,
bulk action bar (move/delete) and backend endpoints, adapted to Bootstrap 5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 09:49:31 +01:00
parent e109795eb4
commit ed3c616676

91
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
@@ -578,6 +578,7 @@ def library(project_id: Optional[str] = None, q_title: str = "", q_content: str
rows = "".join(
[
f"<tr>"
f"<td style='width:36px'><input type='checkbox' class='form-check-input row-cb' value='{d['id']}'></td>"
f"<td>#{d['id']}</td>"
f"<td>{d['title']}</td>"
f"<td>{d['kind']}</td>"
@@ -619,20 +620,75 @@ def library(project_id: Optional[str] = None, q_title: str = "", q_content: str
<div class='col-6 col-md-1 d-grid'><a class='btn btn-outline-secondary' href='/library'>Reset</a></div>
</div>
</form>
<div id='bulkBar' class='alert alert-primary d-none d-flex align-items-center gap-2 py-2 mb-2' role='alert'>
<strong id='bulkCount'>0 ausgewählt</strong>
<button type='button' class='btn btn-sm btn-primary' onclick='bulkMove()'><i class='bi bi-folder-symlink'></i> Verschieben</button>
<button type='button' class='btn btn-sm btn-danger' onclick='bulkDelete()'><i class='bi bi-trash'></i> Löschen</button>
</div>
<div class='card'>
<div class='table-responsive'>
<table class='table table-sm table-striped align-middle mb-0'>
<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' class='text-secondary'>Keine Einträge.</td></tr>"}</tbody>
<thead><tr>
<th style='width:36px'><input type='checkbox' class='form-check-input' 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' class='text-secondary'>Keine Einträge.</td></tr>"}</tbody>
</table>
</div>
</div>
<div class='d-flex gap-2 mt-2'>
<button type='button' class='btn btn-sm btn-outline-secondary' onclick='selectAll()'><i class='bi bi-check2-all'></i> Alle wählen</button>
<button type='button' class='btn btn-sm btn-outline-secondary' onclick='selectNone()'><i class='bi bi-x-square'></i> 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 all = document.querySelectorAll('.row-cb');
document.getElementById('bulkCount').textContent = ids.length + ' ausgewählt';
document.getElementById('bulkBar').classList.toggle('d-none', ids.length === 0);
const cbAll = document.getElementById('cbAll');
cbAll.indeterminate = ids.length > 0 && ids.length < all.length;
cbAll.checked = all.length > 0 && ids.length === all.length;
}}
document.querySelectorAll('.row-cb').forEach(cb => {{
cb.addEventListener('change', function() {{
this.closest('tr').classList.toggle('table-active', this.checked);
updateBulkBar();
}});
}});
document.getElementById('cbAll').addEventListener('change', function() {{
document.querySelectorAll('.row-cb').forEach(cb => {{
cb.checked = this.checked;
cb.closest('tr').classList.toggle('table-active', this.checked);
}});
updateBulkBar();
}});
function selectAll() {{
document.querySelectorAll('.row-cb').forEach(cb => {{ cb.checked=true; cb.closest('tr').classList.add('table-active'); }});
updateBulkBar();
}}
function selectNone() {{
document.querySelectorAll('.row-cb').forEach(cb => {{ cb.checked=false; cb.closest('tr').classList.remove('table-active'); }});
updateBulkBar();
}}
window.libRename = async function(id, current) {{
const v = await window.uiPrompt('Dokument umbenennen', current || '');
if(v===null) return;
@@ -649,6 +705,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)
@@ -755,6 +826,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: