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 concurrent.futures import ThreadPoolExecutor
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import List, Optional
import markdown as md import markdown as md
import requests import requests
@@ -578,6 +578,7 @@ def library(project_id: Optional[str] = None, q_title: str = "", q_content: str
rows = "".join( rows = "".join(
[ [
f"<tr>" 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['id']}</td>"
f"<td>{d['title']}</td>" f"<td>{d['title']}</td>"
f"<td>{d['kind']}</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 class='col-6 col-md-1 d-grid'><a class='btn btn-outline-secondary' href='/library'>Reset</a></div>
</div> </div>
</form> </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='card'>
<div class='table-responsive'> <div class='table-responsive'>
<table class='table table-sm table-striped align-middle mb-0'> <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> <thead><tr>
<tbody>{rows or "<tr><td colspan='6' class='text-secondary'>Keine Einträge.</td></tr>"}</tbody> <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> </table>
</div> </div>
</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> <script>
async function libPost(url, data) {{ async function libPost(url, data) {{
const r = await fetch(url, {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body:new URLSearchParams(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; }} if(!r.ok) {{ alert('Fehler '+r.status); return; }}
location.reload(); 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) {{ window.libRename = async function(id, current) {{
const v = await window.uiPrompt('Dokument umbenennen', current || ''); const v = await window.uiPrompt('Dokument umbenennen', current || '');
if(v===null) return; if(v===null) return;
@@ -649,6 +705,21 @@ window.libDelete = async function(id) {{
if(!ok) return; if(!ok) return;
await libPost(`/document/${{id}}/delete`, {{}}); 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> </script>
""" """
return layout("Library", body) return layout("Library", body)
@@ -755,6 +826,20 @@ def delete_document(doc_id: int):
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>") 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) @app.get("/prompts", response_class=HTMLResponse)
def prompts_page(): def prompts_page():
with db() as c: with db() as c: