From efb0d2aa012da6863df1f0d3c2492e7376d9b5c3 Mon Sep 17 00:00:00 2001 From: alientim Date: Sat, 22 Aug 2026 19:17:32 +0200 Subject: [PATCH] Gruppen-Bearbeiten als Modal, Mobile-Fix, Mehrfach-Upload (v1.1.3) - Gruppen bearbeiten (Name + Rechte) laeuft jetzt ueber ein Modal statt der bisherigen Inline-Ausklapp-Zeile -- gleiche Optik wie "Neue Gruppe". Der Gruppenname war dabei bisher ein verstecktes, nie wirklich editierbares Feld; jetzt ein normales Texteingabefeld (Backend unterstuetzte das Umbenennen inkl. Systemgruppen-Schutz bereits vollstaendig, es fehlte nur die Eingabemoeglichkeit im Formular). Admin-Rechte-Ansicht und die "Freischalten"-Ausnahme fuer die Systemgruppe "Benutzer" ziehen ins jeweilige Modal mit um. - Fileshare-Baum + Tabelle nebeneinander sprengte auf Tablet-/Handybreite die Seite -- stapelt jetzt (Baum oben, Tabelle darunter) ab der bestehenden 900px-Sidebar-Umschaltgrenze. - Mehrfach-Upload: Datei-Eingabefeld erlaubt jetzt echte Mehrfachauswahl (mehrere Dateien in einem Dialog) UND mehrmaliges Hinzufuegen nacheinander (per DataTransfer angesammelt, bevor "Hochladen" gedrueckt wird) -- funktioniert nativ auch auf Mobilgeraeten, da kein Custom- Upload-Mechanismus noetig ist. Backend verarbeitet jetzt eine Liste statt einer einzelnen Datei (request.files.getlist), mit Sammel- Erfolgsmeldung und pro Datei separater Namensvalidierung. Live auf POETEST verifiziert: Umbenennen+Rechte-Speichern ueber das neue Modal, Admin-Modal (readonly), Mobile-Layout (390px, Baum stapelt korrekt), echte Mehrfachauswahl (2 Dateien in einem Dialog, beide korrekt hochgeladen und einzeln in der Sammelmeldung genannt). Co-Authored-By: Claude Sonnet 5 --- srv/tesm/VERSION | 2 +- srv/tesm/app.py | 31 ++++-- srv/tesm/static/css/style.css | 24 +++++ srv/tesm/templates/fileshare.html | 73 ++++++++++++- srv/tesm/templates/groups.html | 167 ++++++++++++++++++------------ 5 files changed, 214 insertions(+), 83 deletions(-) diff --git a/srv/tesm/VERSION b/srv/tesm/VERSION index 45a1b3f..781dcb0 100644 --- a/srv/tesm/VERSION +++ b/srv/tesm/VERSION @@ -1 +1 @@ -1.1.2 +1.1.3 diff --git a/srv/tesm/app.py b/srv/tesm/app.py index 8281c5b..f035d7e 100644 --- a/srv/tesm/app.py +++ b/srv/tesm/app.py @@ -2289,18 +2289,31 @@ def fileshare_upload(): return redirect(url_for("fileshare", share=share, path=rel_path)) abs_dir = _fileshare_resolve_path(share, rel_path) - file = request.files.get("file") - if not abs_dir or not os.path.isdir(abs_dir) or not file or not file.filename: + # request.files.getlist() statt .get(): das Upload-Feld erlaubt jetzt + # Mehrfachauswahl (name="file" multiple) -- ein einzelner Dateiauswahl- + # Dialog liefert dann mehrere Files unter demselben Feldnamen, klassisch + # eine Datei liefert genauso eine Liste mit einem Element. + files = [f for f in request.files.getlist("file") if f and f.filename] + if not abs_dir or not os.path.isdir(abs_dir) or not files: flash("Ungültiges Ziel oder keine Datei ausgewählt.", "danger") else: - filename = secure_filename(file.filename) - dest = os.path.join(abs_dir, filename) if filename else None - if not filename or os.path.dirname(os.path.realpath(dest)) != os.path.realpath(abs_dir): - flash("Ungültiger Dateiname.", "danger") - else: + uploaded, rejected = [], [] + for file in files: + filename = secure_filename(file.filename) + dest = os.path.join(abs_dir, filename) if filename else None + if not filename or os.path.dirname(os.path.realpath(dest)) != os.path.realpath(abs_dir): + rejected.append(file.filename) + continue file.save(dest) - log_action("fileshare.upload", share, f"{rel_path}/{filename}".strip("/")) - flash(f"„{filename}“ hochgeladen.", "success") + uploaded.append(filename) + if uploaded: + log_action("fileshare.upload", share, f"{rel_path}/".strip("/") + f" ({len(uploaded)} Datei(en): {', '.join(uploaded)})") + if len(uploaded) == 1: + flash(f"„{uploaded[0]}“ hochgeladen.", "success") + else: + flash(f"{len(uploaded)} Dateien hochgeladen: {', '.join(uploaded)}.", "success") + if rejected: + flash(f"Ungültiger Dateiname, übersprungen: {', '.join(rejected)}.", "danger") return redirect(url_for("fileshare", share=share, path=rel_path)) diff --git a/srv/tesm/static/css/style.css b/srv/tesm/static/css/style.css index 530d6e5..98b328e 100644 --- a/srv/tesm/static/css/style.css +++ b/srv/tesm/static/css/style.css @@ -1360,6 +1360,24 @@ select { .xlsx-preview-sheet-title { margin: 20px 0 8px; font-size: 13px; font-weight: 650; } .xlsx-preview-sheet-title:first-child { margin-top: 0; } +/* Angesammelte Dateien im Upload-Modal (Mehrfachauswahl, siehe fileshare.html) */ +.upload-file-list { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; } +.upload-file-row { + display: flex; align-items: center; justify-content: space-between; gap: 8px; + padding: 5px 10px; + background: var(--bg-elevated); + border: 1px solid var(--border-soft); + border-radius: 7px; + font-size: 12.5px; +} +.upload-file-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.upload-file-remove { + flex-shrink: 0; + border: none; background: transparent; color: var(--text-faint); + font-size: 16px; line-height: 1; cursor: pointer; padding: 0 2px; +} +.upload-file-remove:hover { color: var(--danger); } + /* ========================================================================== Utilities ========================================================================== */ @@ -1404,6 +1422,12 @@ select { Buttons/Suchfeld ineinanderzuschieben. */ .modal-footer { flex-wrap: wrap; } .table-toolbar .search-input { min-width: 0; flex: 1 1 160px; } + + /* Fileshare-Baum + Tabelle nebeneinander sprengt auf Tablet-/Handy- + Breite die Seite (Baum-Spalte ist fest 260px breit) -- Baum stapelt + stattdessen oben, Tabelle darunter in voller Breite. */ + .fileshare-layout { flex-direction: column; } + .fileshare-tree { flex: 1 1 auto; max-width: 100%; max-height: 240px; } } @media (max-width: 640px) { diff --git a/srv/tesm/templates/fileshare.html b/srv/tesm/templates/fileshare.html index 048bcac..c8d6962 100644 --- a/srv/tesm/templates/fileshare.html +++ b/srv/tesm/templates/fileshare.html @@ -140,14 +140,15 @@ @@ -242,6 +243,70 @@ function filterTable(inputId, tableId) { }); } +/* ---------------- Mehrfach-Upload (Multiauswahl + mehrmals nacheinander) ---------------- */ +/* Ein ERSETZT bei jeder erneuten Dateiauswahl + die vorherige -- fuer "mehrmals nacheinander hinzufuegen" wird deshalb + selbst eine "angesammelte" Auswahl per DataTransfer gepflegt und nach + jeder Aenderung zurueck auf das Input-Feld geschrieben, sodass das + normale
-Submit (kein fetch() noetig) am Ende alle gesammelten + Dateien mitschickt. DataTransfer-Zuweisung an .files wird von allen + gaengigen Mobil-Browsern (Android Chrome, iOS Safari) mitgetragen; falls + nicht, faellt es einfach auf das native Verhalten (letzte Auswahl zaehlt) + zurueck, ohne den Upload an sich zu verhindern. */ +(function () { + const input = document.getElementById("uploadFileInput"); + const listEl = document.getElementById("uploadFileList"); + if (!input || !listEl) return; + let staged = null; + try { staged = new DataTransfer(); } catch (e) { staged = null; } + + function render() { + listEl.innerHTML = ""; + if (!staged) return; + Array.from(staged.files).forEach(function (file, idx) { + const row = document.createElement("div"); + row.className = "upload-file-row"; + const name = document.createElement("span"); + name.textContent = file.name; + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "upload-file-remove"; + removeBtn.setAttribute("aria-label", "Entfernen"); + removeBtn.textContent = "×"; + removeBtn.addEventListener("click", function () { + const dt = new DataTransfer(); + Array.from(staged.files).forEach(function (f, i) { + if (i !== idx) dt.items.add(f); + }); + staged = dt; + input.files = staged.files; + render(); + }); + row.appendChild(name); + row.appendChild(removeBtn); + listEl.appendChild(row); + }); + } + + input.addEventListener("change", function () { + if (!staged) return; // kein DataTransfer-Support -- natives Verhalten greift + Array.from(input.files).forEach(function (file) { staged.items.add(file); }); + input.files = staged.files; + render(); + }); + + // Beim (Wieder-)Oeffnen des Modals eine frische Sammlung starten, statt + // Dateien aus einem vorherigen, bereits abgeschickten Upload-Vorgang + // versehentlich mitzuschleppen. + document.querySelectorAll('[data-open-modal="uploadModal"]').forEach(function (btn) { + btn.addEventListener("click", function () { + try { staged = new DataTransfer(); } catch (e) { staged = null; } + input.value = ""; + render(); + }); + }); +})(); + /* ---------------- Baum-Navigation (Freigaben links) ---------------- */ function buildTreeNode(share, path, name) { diff --git a/srv/tesm/templates/groups.html b/srv/tesm/templates/groups.html index 2d44c41..2ffde9b 100644 --- a/srv/tesm/templates/groups.html +++ b/srv/tesm/templates/groups.html @@ -104,8 +104,8 @@ {{ admin_virtual_group.member_names|length }}
-
- - - {{ permission_tree(admin_virtual_group.permissions, true, true) }} -

Admins dürfen immer alles — diese Rechte sind fest und nicht änderbar.

- - {% for g in groups %} {% set can_edit_this = current_user.has_permission('groups.edit') and not g.is_system %} + {% set can_unlock_system = g.is_system and current_user.is_admin %} @@ -132,8 +127,12 @@ {{ g.member_names|length }}
-
- {% set can_unlock_system = g.is_system and current_user.is_admin %} - - - {% if can_edit_this %} - - - - - - {{ permission_tree(g.permissions, false, true) }} -
- -
-
- {% elif can_unlock_system %} -
- {{ permission_tree(g.permissions, true, true) }} -
-

Die Standardgruppe „Benutzer“ ist eine Systemgruppe — ihre Rechte sind normalerweise fest.

- -
-
- - {% else %} - {{ permission_tree(g.permissions, true, true) }} - {% if g.is_system %} -

Die Standardgruppe „Benutzer“ ist eine Systemgruppe — ihre Rechte sind fest und nicht änderbar.

- {% endif %} - {% endif %} - - {% else %} @@ -210,6 +158,19 @@ + +