feat(projekt): Anheftungen per Drag & Drop zwischen Unterreitern verschieben

Pin-Zeilen in der Ordneransicht sind draggable, die Reiter sind Ablageziele.
Ein Drop schickt PATCH /api/projects/:id/pins/:pinId {tab_id}; movePin schreibt
project_pins.tab_id um. Verschoben wird nur die Referenz.

- Zielebene hat die Aufnahme schon: Quell-Pin loeschen statt an UNIQUE uniq_pin
  scheitern (merged:true)
- Zielreiter serverseitig gegen den Ordner pruefen, sonst 404
- .ptab-n braucht pointer-events:none, sonst flackert die Ablagemarkierung

Verifiziert gegen die laufende App inkl. 404- und Merge-Faellen; Testdaten
zurueckgesetzt. Doku: Kapitel 10 + Changelog.
This commit is contained in:
2026-08-14 14:52:14 +02:00
parent 2be938c91b
commit 3d628b4b96
6 changed files with 1842 additions and 1678 deletions
+49 -4
View File
@@ -96,6 +96,49 @@ Ein Klick auf eine angeheftete Aufnahme springt in den Reiter „Aufnahmen" und
Detailbereich (`jumpToRecording`, dieselbe Mechanik wie der Sprung aus einem Suchtreffer, nur ohne
Zeitmarke).
## Verschieben per Drag & Drop (2026-08-14)
Eine angeheftete Aufnahme lässt sich in der Ordneransicht auf einen **anderen Reiter der Reiterleiste
ziehen** und wechselt damit die Ebene — z. B. von „Projektleiter Runden" nach „INF-Projektboard".
Verschoben wird nur der Pin (`project_pins.tab_id`); die Aufnahme selbst wird nicht angefasst.
```mermaid
sequenceDiagram
participant U as Nutzer
participant G as GUI (main.js)
participant S as server.js
participant D as MariaDB
U->>G: dragstart auf 📌-Zeile
Note over G: merkt pin_id,<br/>setzt dataTransfer (Firefox braucht das)
U->>G: dragover Zielreiter
G-->>U: Reiter gestrichelt markiert (.ptab.drop)<br/>eigener Reiter = kein Ziel
U->>G: drop
G->>S: PATCH /api/projects/:id/pins/:pinId {tab_id}
S->>D: db.movePin()
alt Zielreiter gehört nicht zum Ordner / Pin unbekannt
D-->>S: null
S-->>G: 404
else Aufnahme hängt dort schon
D->>D: DELETE Quell-Pin (Zusammenführen)
S-->>G: {moved:true, merged:true}
else Normalfall
D->>D: UPDATE project_pins SET tab_id
S-->>G: {moved:true}
end
G->>G: renderProjectDetail() neu zeichnen
```
Drei Details, die nicht offensichtlich sind:
- **Zusammenführen statt Duplikat:** hängt dieselbe Aufnahme auf der Zielebene bereits, würde das `UPDATE`
an `UNIQUE uniq_pin` scheitern. `movePin` löscht in diesem Fall den Quell-Pin, statt einen Fehler zu
werfen — im Ergebnis ist die Aufnahme genau einmal auf der Zielebene, was der Nutzer erwartet hat.
- **Ziel wird serverseitig geprüft:** `tab_id` muss ein Reiter *desselben* Ordners sein (oder 0 für die
Ordner-Ebene), sonst 404. Ein veralteter Reiter im offenen Browser-Tab kann so keinen Pin in ein
fremdes Projekt schieben.
- **`.ptab-n` (der Zähler im Reiter) bekommt `pointer-events: none`.** Sonst feuert beim Überfahren des
Zählers ein `dragleave` am Reiter und die Ablagemarkierung flackert.
## API
| Route | Zweck |
@@ -109,13 +152,15 @@ Zeitmarke).
| `PATCH /api/projects/:id/tabs/:tid` | Unterreiter umbenennen; `:tid = 0` benennt die Ordner-Ebene um (leerer Name = zurück auf „Ordner") |
| `DELETE /api/projects/:id/tabs/:tid` | Unterreiter löschen |
| `POST /api/projects/:id/pins` | anheften `{recording_id, tab_id}` (`tab_id` fehlt/0 = Ordner-Ebene) |
| `PATCH /api/projects/:id/pins/:pinId` | Anheftung auf eine andere Ebene desselben Ordners verschieben `{tab_id}` (Drag & Drop); 404 bei fremdem Reiter/Ordner, `{merged:true}` wenn dort schon vorhanden |
| `DELETE /api/projects/:id/pins/:pinId` | Anheftung lösen |
| `GET /api/recordings/:id/pins` | wo hängt diese Aufnahme überall |
| `GET /api/pins/counts` | `{recording_id: anzahl}` für die ganze Liste |
## Dateien
`app/db.js` (Tabellen + CRUD), `app/server.js` (Routen), `app/web/src/main.js`
(`viewProjects`/`renderProjectList`/`renderProjectDetail`/`pinPickRecordingDialog`/`pinToProjectDialog`/
`openModal`), `app/web/src/style.css` (Ordnergitter, Reiterleiste, Dialog).
Board-Backups: `.bak-projects`.
`app/db.js` (Tabellen + CRUD, `movePin`), `app/server.js` (Routen), `app/web/src/main.js`
(`viewProjects`/`renderProjectList`/`renderProjectDetail`/`bindPinDrag`/`pinPickRecordingDialog`/
`pinToProjectDialog`/`openModal`), `app/web/src/style.css` (Ordnergitter, Reiterleiste, `.ptab.drop`,
`.item.rec.dragging`, Dialog).
Board-Backups: `.bak-projects` (Grundfunktion), `.bak-movepin` (Drag & Drop).