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:
@@ -208,6 +208,33 @@ export async function pinRecording(projectId, tabId, recordingId) {
|
||||
export async function unpinRecording(pinId) {
|
||||
await pool.execute('DELETE FROM project_pins WHERE id=?', [pinId]);
|
||||
}
|
||||
// Einen Pin auf eine andere Ebene DESSELBEN Ordners schieben (Drag&Drop in der
|
||||
// Projektansicht). Hängt die Aufnahme auf der Zielebene schon, entsteht kein
|
||||
// zweiter Pin — der Quell-Pin verschwindet stattdessen (Zusammenführen statt
|
||||
// Duplikat; ein UPDATE würde sonst an UNIQUE uniq_pin scheitern).
|
||||
export async function movePin(pinId, projectId, tabId) {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT tab_id, recording_id FROM project_pins WHERE id=? AND project_id=?', [pinId, projectId]);
|
||||
if (!rows.length) return null;
|
||||
const src = rows[0];
|
||||
const dst = tabId || 0;
|
||||
// Zielreiter muss zu diesem Ordner gehören (0 = Ordner-Ebene, gibt es immer).
|
||||
if (dst) {
|
||||
const [t] = await pool.execute(
|
||||
'SELECT id FROM project_tabs WHERE id=? AND project_id=?', [dst, projectId]);
|
||||
if (!t.length) return null;
|
||||
}
|
||||
if (src.tab_id === dst) return { ok: true, moved: false, tab_id: dst };
|
||||
const [dup] = await pool.execute(
|
||||
'SELECT id FROM project_pins WHERE project_id=? AND tab_id=? AND recording_id=?',
|
||||
[projectId, dst, src.recording_id]);
|
||||
if (dup.length) {
|
||||
await pool.execute('DELETE FROM project_pins WHERE id=?', [pinId]);
|
||||
return { ok: true, moved: true, merged: true, tab_id: dst, pin_id: dup[0].id };
|
||||
}
|
||||
await pool.execute('UPDATE project_pins SET tab_id=? WHERE id=?', [dst, pinId]);
|
||||
return { ok: true, moved: true, tab_id: dst, pin_id: pinId };
|
||||
}
|
||||
// Wo hängt diese Aufnahme überall? Treibt die Markierung des 📌-Buttons in der
|
||||
// Aufnahmenliste.
|
||||
export async function listPinsForRecording(recId) {
|
||||
|
||||
@@ -541,6 +541,14 @@ app.post('/api/projects/:id/pins', wrap(async (req, res) => {
|
||||
await db.pinRecording(+req.params.id, +((req.body && req.body.tab_id) || 0), recId);
|
||||
res.json({ ok: true });
|
||||
}));
|
||||
// Drag&Drop in der Projektansicht: Pin auf eine andere Ebene desselben Ordners
|
||||
// ziehen. tab_id 0 = zurück auf die Ordner-Ebene.
|
||||
app.patch('/api/projects/:id/pins/:pinId', wrap(async (req, res) => {
|
||||
const tabId = +((req.body && req.body.tab_id) || 0);
|
||||
const r = await db.movePin(+req.params.pinId, +req.params.id, tabId);
|
||||
if (!r) return res.status(404).json({ error: 'Anheftung oder Zielreiter nicht gefunden' });
|
||||
res.json(r);
|
||||
}));
|
||||
app.delete('/api/projects/:id/pins/:pinId', wrap(async (req, res) => {
|
||||
await db.unpinRecording(+req.params.pinId);
|
||||
res.json({ ok: true });
|
||||
|
||||
+1716
-1673
File diff suppressed because it is too large
Load Diff
@@ -527,12 +527,20 @@ body.resizing { cursor: col-resize; user-select: none; }
|
||||
.ptab.active { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); font-weight: 600; }
|
||||
.ptab.add { color: var(--muted); border-style: dashed; border-color: var(--line); }
|
||||
.ptab.add:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.ptab-n { font-size: 11px; opacity: .7; margin-left: 4px; }
|
||||
/* pointer-events aus: sonst feuert beim Ueberfahren des Zaehlers ein
|
||||
dragleave am Reiter und die Ablagemarkierung flackert. */
|
||||
.ptab-n { font-size: 11px; opacity: .7; margin-left: 4px; pointer-events: none; }
|
||||
/* Reiter als Ablageziel, waehrend ein Pin darueber gezogen wird. */
|
||||
.ptab.drop {
|
||||
color: var(--accent); background: var(--accent-soft);
|
||||
border-color: var(--accent); border-style: dashed;
|
||||
}
|
||||
|
||||
.proj-level { margin-top: 14px; }
|
||||
.proj-level-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.proj-level-head .grow { flex: 1; }
|
||||
.proj-level .item.rec { cursor: pointer; }
|
||||
.proj-level .item.rec.dragging { opacity: .45; border-style: dashed; }
|
||||
|
||||
/* Stecknadel in der Aufnahmenliste: blass, solange die Aufnahme nirgends
|
||||
hängt — angeheftet wird sie gruen (dieselbe Farbe wie „KI ✓“, also
|
||||
|
||||
Reference in New Issue
Block a user