Add CSV upload for references
This commit is contained in:
parent
b426020a76
commit
46f2a303dc
2 changed files with 143 additions and 1 deletions
111
app.js
111
app.js
|
|
@ -1557,6 +1557,117 @@ async function importCSVRows(rows) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── References CSV upload ──────────────────────────────────
|
||||||
|
const refsCsvFileInput = $('refs-csv-file-input');
|
||||||
|
|
||||||
|
$('btn-upload-refs-csv').addEventListener('click', () => {
|
||||||
|
bulkEditPopover.hidden = true;
|
||||||
|
openUploadRefsCsvModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
refsCsvFileInput.addEventListener('change', () => {
|
||||||
|
$('btn-refs-csv-import').disabled = !refsCsvFileInput.files.length;
|
||||||
|
$('refs-csv-results').hidden = true;
|
||||||
|
$('refs-csv-results').innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btn-refs-csv-cancel').addEventListener('click', () => $('upload-refs-csv-modal').close());
|
||||||
|
|
||||||
|
$('btn-refs-csv-import').addEventListener('click', async () => {
|
||||||
|
const file = refsCsvFileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
$('btn-refs-csv-import').disabled = true;
|
||||||
|
$('btn-refs-csv-cancel').textContent = 'Close';
|
||||||
|
const resultsEl = $('refs-csv-results');
|
||||||
|
resultsEl.hidden = false;
|
||||||
|
resultsEl.innerHTML = '<p class="csv-status">Importing…</p>';
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const rows = parseCSV(text);
|
||||||
|
if (!rows.length) { resultsEl.innerHTML = '<p class="csv-error">No data rows found.</p>'; return; }
|
||||||
|
const results = await importRefsCsvRows(rows);
|
||||||
|
await loadAll();
|
||||||
|
let html = `<p class="csv-ok">✓ ${results.created} reference${results.created !== 1 ? 's' : ''} imported.</p>`;
|
||||||
|
if (results.warnings.length) {
|
||||||
|
html += `<p class="csv-error">${results.warnings.length} skipped — tune not found:</p><ul class="csv-errors">`;
|
||||||
|
results.warnings.forEach(w => { html += `<li>${esc(w)}</li>`; });
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
if (results.errors.length) {
|
||||||
|
html += `<p class="csv-error">${results.errors.length} error${results.errors.length !== 1 ? 's' : ''}:</p><ul class="csv-errors">`;
|
||||||
|
results.errors.forEach(e => { html += `<li>${esc(e.name)}: ${esc(e.error)}</li>`; });
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
resultsEl.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
resultsEl.innerHTML = `<p class="csv-error">Failed: ${esc(e.message)}</p>`;
|
||||||
|
$('btn-refs-csv-import').disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openUploadRefsCsvModal() {
|
||||||
|
refsCsvFileInput.value = '';
|
||||||
|
$('btn-refs-csv-import').disabled = true;
|
||||||
|
$('btn-refs-csv-cancel').textContent = 'Cancel';
|
||||||
|
$('refs-csv-results').hidden = true;
|
||||||
|
$('refs-csv-results').innerHTML = '';
|
||||||
|
$('refs-csv-format-details').open = true;
|
||||||
|
$('upload-refs-csv-modal').showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importRefsCsvRows(rows) {
|
||||||
|
const siteCache = new Map();
|
||||||
|
const musCache = new Map();
|
||||||
|
const results = { created: 0, warnings: [], errors: [] };
|
||||||
|
|
||||||
|
async function resolve(endpoint, list, cache, name) {
|
||||||
|
if (!name) return null;
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
if (cache.has(lower)) return cache.get(lower);
|
||||||
|
const existing = list.find(x => x.name.toLowerCase() === lower);
|
||||||
|
if (existing) { cache.set(lower, existing.id); return existing.id; }
|
||||||
|
const item = await apiFetch(endpoint, { method: 'POST', body: JSON.stringify({ name }) });
|
||||||
|
list.push(item);
|
||||||
|
cache.set(lower, item.id);
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const tuneName = row.tune_name?.trim();
|
||||||
|
if (!tuneName) continue;
|
||||||
|
|
||||||
|
const tune = allTunes.find(t => (t.name ?? '').toLowerCase() === tuneName.toLowerCase());
|
||||||
|
if (!tune) {
|
||||||
|
results.warnings.push(tuneName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const link = row.link?.trim() || null;
|
||||||
|
const siteId = await resolve('/reference_sites/', allReferenceSites, siteCache, row.site?.trim() || null);
|
||||||
|
|
||||||
|
const musicians = [];
|
||||||
|
for (const key of ['musician1', 'musician2', 'musician3', 'musician4']) {
|
||||||
|
const name = row[key]?.trim();
|
||||||
|
if (!name) continue;
|
||||||
|
const id = await resolve('/musicians/', allMusicians, musCache, name);
|
||||||
|
if (id) musicians.push({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!link && !siteId && musicians.length === 0) continue;
|
||||||
|
|
||||||
|
await apiFetch(`/tunes/${tune.id}/references`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ link, site_id: siteId, musicians }),
|
||||||
|
});
|
||||||
|
results.created++;
|
||||||
|
} catch (e) {
|
||||||
|
results.errors.push({ name: tuneName, error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Mobile truncation tooltip ───────────────────────────────────
|
// ── Mobile truncation tooltip ───────────────────────────────────
|
||||||
{
|
{
|
||||||
const tip = $('mob-tooltip');
|
const tip = $('mob-tooltip');
|
||||||
|
|
|
||||||
33
index.html
33
index.html
|
|
@ -51,7 +51,8 @@
|
||||||
<!-- ── Bulk edit popover ─────────────────────────────────────── -->
|
<!-- ── Bulk edit popover ─────────────────────────────────────── -->
|
||||||
<div id="bulk-edit-popover" hidden>
|
<div id="bulk-edit-popover" hidden>
|
||||||
<button id="btn-open-bulk-refs">📎 Bulk Edit References</button>
|
<button id="btn-open-bulk-refs">📎 Bulk Edit References</button>
|
||||||
<button id="btn-upload-csv">📂 Upload from CSV</button>
|
<button id="btn-upload-csv">📂 Upload Tunes from CSV</button>
|
||||||
|
<button id="btn-upload-refs-csv">🔗 Upload References from CSV</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Bulk edit references modal ───────────────────────────── -->
|
<!-- ── Bulk edit references modal ───────────────────────────── -->
|
||||||
|
|
@ -255,6 +256,36 @@
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- ── References CSV upload modal ───────────────────────────── -->
|
||||||
|
<dialog id="upload-refs-csv-modal">
|
||||||
|
<h2>Upload References from CSV</h2>
|
||||||
|
<details id="refs-csv-format-details" open>
|
||||||
|
<summary>Expected format</summary>
|
||||||
|
<p style="font-size:.85rem;margin:.5rem 0 .4rem">One reference per row. Tune matched by name — must already exist.</p>
|
||||||
|
<table class="csv-format-table">
|
||||||
|
<thead><tr><th>Column</th><th>Required</th><th>Notes</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>tune_name</td><td>Yes</td><td>Must match an existing tune (case-insensitive)</td></tr>
|
||||||
|
<tr><td>link</td><td></td><td>URL</td></tr>
|
||||||
|
<tr><td>site</td><td></td><td>Site name — created if new</td></tr>
|
||||||
|
<tr><td>musician1</td><td></td><td>Musician name — created if new</td></tr>
|
||||||
|
<tr><td>musician2</td><td></td><td></td></tr>
|
||||||
|
<tr><td>musician3</td><td></td><td></td></tr>
|
||||||
|
<tr><td>musician4</td><td></td><td></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="csv-format-note">Rows where the tune name is not found are skipped with a warning in the results.</p>
|
||||||
|
</details>
|
||||||
|
<div id="refs-csv-upload-area">
|
||||||
|
<input type="file" id="refs-csv-file-input" accept=".csv,.tsv,text/csv" />
|
||||||
|
</div>
|
||||||
|
<div id="refs-csv-results" hidden></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="btn-refs-csv-import" disabled>Import</button>
|
||||||
|
<button id="btn-refs-csv-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Tooltip for truncated mobile text -->
|
<!-- Tooltip for truncated mobile text -->
|
||||||
<div id="mob-tooltip" hidden></div>
|
<div id="mob-tooltip" hidden></div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue