diff --git a/app/db.js b/app/db.js index 35af3e2..c8709b0 100644 --- a/app/db.js +++ b/app/db.js @@ -89,6 +89,10 @@ export async function ensureSchema() { id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(190) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`); + // Beschriftung der Ordner-Ebene (tab_id 0). Die liegt am Projekt statt in + // project_tabs, weil sie keine echte Reiter-Zeile ist — es gibt sie immer, + // sie lässt sich nicht löschen und hat keine eigene id. NULL = „Ordner". + await pool.query('ALTER TABLE projects ADD COLUMN IF NOT EXISTS root_tab_name VARCHAR(190) NULL'); await pool.query(`CREATE TABLE IF NOT EXISTS project_tabs ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, @@ -113,7 +117,7 @@ export async function ensureSchema() { // der (nicht gelöschten) Pins über alle Ebenen. export async function listProjects() { const [rows] = await pool.query(` - SELECT p.id, p.name, p.created_at, + SELECT p.id, p.name, p.root_tab_name, p.created_at, (SELECT COUNT(*) FROM project_tabs t WHERE t.project_id = p.id) AS tabs, (SELECT COUNT(*) FROM project_pins pin JOIN recordings r ON r.id = pin.recording_id WHERE pin.project_id = p.id AND r.deleted_at IS NULL) AS pins @@ -128,6 +132,12 @@ export async function createProject(name) { export async function renameProject(id, name) { await pool.execute('UPDATE projects SET name=? WHERE id=?', [String(name).slice(0, 190), id]); } +// Ordner-Ebene umbenennen. Leerer Name setzt auf die Vorgabe „Ordner" zurück. +export async function renameProjectRoot(id, name) { + const n = String(name || '').trim().slice(0, 190); + await pool.execute('UPDATE projects SET root_tab_name=? WHERE id=?', [n || null, id]); + return n || null; +} // Der Ordner verschwindet mitsamt Reitern und Pins — die Aufnahmen selbst // bleiben unberührt (ein Pin ist nur eine Referenz). export async function deleteProject(id) { @@ -153,7 +163,7 @@ export async function deleteProjectTab(id) { // (im Papierkorb liegende) Aufnahmen werden ausgeblendet, nicht entfernt: eine // Wiederherstellung bringt den Pin ohne Zutun zurück. export async function getProject(id) { - const [pr] = await pool.query('SELECT id, name, created_at FROM projects WHERE id=?', [id]); + const [pr] = await pool.query('SELECT id, name, root_tab_name, created_at FROM projects WHERE id=?', [id]); if (!pr.length) return null; const [tabs] = await pool.query( 'SELECT id, name, created_at FROM project_tabs WHERE project_id=? ORDER BY id', [id]); @@ -179,7 +189,7 @@ export async function unpinRecording(pinId) { export async function listPinsForRecording(recId) { const [rows] = await pool.query(` SELECT pin.id AS pin_id, pin.project_id, p.name AS project_name, - pin.tab_id, t.name AS tab_name + pin.tab_id, COALESCE(t.name, p.root_tab_name) AS tab_name FROM project_pins pin JOIN projects p ON p.id = pin.project_id LEFT JOIN project_tabs t ON t.id = pin.tab_id diff --git a/app/server.js b/app/server.js index a87bc76..f21ffdc 100644 --- a/app/server.js +++ b/app/server.js @@ -479,8 +479,15 @@ app.post('/api/projects/:id/tabs', wrap(async (req, res) => { if (!name) return res.status(400).json({ error: 'name fehlt' }); res.json(await db.createProjectTab(+req.params.id, name)); })); +// tid 0 = die Ordner-Ebene. Die ist keine Zeile in project_tabs (es gibt sie +// immer, sie lässt sich nicht löschen), ihre Beschriftung hängt am Projekt. +// Leerer Name setzt sie auf die Vorgabe „Ordner" zurück. app.patch('/api/projects/:id/tabs/:tid', wrap(async (req, res) => { const name = String((req.body && req.body.name) || '').trim(); + if (+req.params.tid === 0) { + const n = await db.renameProjectRoot(+req.params.id, name); + return res.json({ ok: true, id: 0, name: n }); + } if (!name) return res.status(400).json({ error: 'name fehlt' }); await db.renameProjectTab(+req.params.tid, name); res.json({ ok: true, id: +req.params.tid, name }); diff --git a/app/web/src/main.js b/app/web/src/main.js index 63fe622..9f3b3a3 100644 --- a/app/web/src/main.js +++ b/app/web/src/main.js @@ -575,7 +575,9 @@ async function renderProjectDetail(v) { if (state.projectTabId && !p.tabs.some(t => t.id === state.projectTabId)) state.projectTabId = 0; const cur = state.projectTabId; const pins = p.pins.filter(x => x.tab_id === cur); - const levelName = cur ? (p.tabs.find(t => t.id === cur) || {}).name : p.name; + // Die Ordner-Ebene ist umbenennbar; ohne eigenen Namen heißt sie „Ordner". + const rootName = p.root_tab_name || 'Ordner'; + const levelName = cur ? (p.tabs.find(t => t.id === cur) || {}).name : rootName; v.innerHTML = `
@@ -584,7 +586,8 @@ async function renderProjectDetail(v) {
- + ${p.tabs.map(t => ``).join('')} @@ -594,8 +597,8 @@ async function renderProjectDetail(v) { ${escapeHtml(levelName)} ${pins.length} angeheftete Aufnahme${pins.length === 1 ? '' : 'n'} - ${cur ? ` - ` : ''} + + ${cur ? '' : ''}
${pins.length ? pins.map(r => ` @@ -630,8 +633,10 @@ async function renderProjectDetail(v) { }; const renBtn = v.querySelector('#ptabren'); if (renBtn) renBtn.onclick = async () => { - const name = prompt('Unterreiter umbenennen:', levelName); - if (name === null || !name.trim()) return; + // Auf der Ordner-Ebene ist ein leeres Feld erlaubt: es setzt die + // Beschriftung auf die Vorgabe „Ordner" zurück. + const name = prompt(cur ? 'Unterreiter umbenennen:' : 'Ordner-Ebene umbenennen (leer = „Ordner"):', levelName); + if (name === null || (cur && !name.trim())) return; await api.patch(`/api/projects/${p.id}/tabs/${cur}`, { name: name.trim() }); renderProjectDetail(v); }; @@ -719,7 +724,7 @@ async function pinToProjectDialog(rec, done) { const loadTabs = async () => { if (pp.value === 'new') { pt.innerHTML = ''; return; } const p = await api.get('/api/projects/' + pp.value); - pt.innerHTML = '' + + pt.innerHTML = `` + p.tabs.map(t => ``).join(''); }; pp.onchange = async () => { diff --git a/docs/08-changelog.md b/docs/08-changelog.md index 0682767..c74fc55 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,24 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). +## 2026-08-02 (nachgereicht) — Ordner-Ebene umbenennbar + +Der erste Reiter eines Projekts hieß fest „Ordner". Er trägt jetzt einen frei wählbaren Namen, genau wie +die Unterreiter. + +Der Name steht in der **neuen Spalte `projects.root_tab_name`** (NULL = „Ordner") und nicht als Zeile in +`project_tabs`: die Ordner-Ebene ist kein echter Reiter — es gibt sie immer, sie hat keine eigene id und +lässt sich nicht löschen. Eine Zeile in `project_tabs` hätte für sie eine id gebraucht, die dann in +`project_pins.tab_id` auftauchen müsste, womit die „0 = Ordner-Ebene"-Regel und mit ihr der +`UNIQUE`-Index gefallen wären. + +`PATCH /api/projects/:id/tabs/0` fängt die 0 ab und schreibt ins Projekt; ein leerer Name setzt zurück auf +„Ordner". Auch die Ebenen-Auswahl im 📌-Dialog und die Liste bestehender Anheftungen zeigen den Namen +(`listPinsForRecording` per `COALESCE(t.name, p.root_tab_name)`). + +Dateien: `app/db.js`, `app/server.js`, `app/web/src/main.js`, Kapitel +[10 – Projektansicht](10-projektansicht.md). + ## 2026-08-02 — Neuer Reiter „Projektansicht": Ordner, Unterreiter, angeheftete Aufnahmen **Was:** Aufnahmen lassen sich jetzt in Projektordnern bündeln. Ein Ordner („Project X") nimmt direkt diff --git a/docs/10-projektansicht.md b/docs/10-projektansicht.md index 34bf962..56d1169 100644 --- a/docs/10-projektansicht.md +++ b/docs/10-projektansicht.md @@ -23,12 +23,18 @@ flowchart TD Zwei Ebenen, mehr nicht: Ordner und darin Unterreiter. Auf **beiden** kann angeheftet werden — der Ordner selbst ist also nicht nur ein Behälter für Reiter. +Die Ordner-Ebene ist genauso benennbar wie ein Unterreiter (Vorgabe: „Ordner"). Ihr Name steht aber in +`projects.root_tab_name` und nicht als Zeile in `project_tabs`, weil sie kein echter Reiter ist: es gibt +sie immer, sie hat keine eigene id und lässt sich nicht löschen. `PATCH …/tabs/0` schreibt deshalb ins +Projekt; ein leerer Name setzt auf „Ordner" zurück. + ## Datenbank ```sql CREATE TABLE projects ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(190) NOT NULL, + root_tab_name VARCHAR(190) NULL, -- Beschriftung der Ordner-Ebene, NULL = „Ordner" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE TABLE project_tabs ( @@ -98,7 +104,7 @@ Zeitmarke). | `PATCH /api/projects/:id` | Ordner umbenennen | | `DELETE /api/projects/:id` | Ordner löschen (Reiter + Anheftungen mit) | | `POST /api/projects/:id/tabs` | Unterreiter anlegen `{name}` | -| `PATCH /api/projects/:id/tabs/:tid` | Unterreiter umbenennen | +| `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) | | `DELETE /api/projects/:id/pins/:pinId` | Anheftung lösen |