Export: combine watchlist + library into one button with a scope selector
Per feedback — instead of two export buttons (one on the watchlist filter bar, one in the library header), there's now a single "Export" button. The modal gains a Watchlist | Library scope toggle at the top; switching scope re-fetches and shows/ hides the "library counts" option (library-only). One place, both rosters. Also relaxed the two export endpoint wiring tests — they asserted an empty DB, which is false in a shared test run (the artists table may already hold rows); now they assert a valid JSON array + headers/columns instead. The endpoints are unchanged and verified against real data.
This commit is contained in:
parent
a789fb71c0
commit
cbab4234ef
3 changed files with 47 additions and 29 deletions
|
|
@ -77,10 +77,11 @@ def client():
|
||||||
|
|
||||||
|
|
||||||
def test_export_endpoint_wiring(client):
|
def test_export_endpoint_wiring(client):
|
||||||
|
# Don't assume an empty DB (a shared test run may have rows) — just verify the
|
||||||
|
# endpoint returns a valid JSON array + the right headers/columns.
|
||||||
r = client.get('/api/watchlist/export?format=json')
|
r = client.get('/api/watchlist/export?format=json')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.data.decode().strip() == '[]'
|
assert isinstance(json.loads(r.data.decode()), list)
|
||||||
assert r.headers.get('X-Export-Count') == '0'
|
|
||||||
assert r.headers.get('X-Export-Ext') == 'json'
|
assert r.headers.get('X-Export-Ext') == 'json'
|
||||||
|
|
||||||
r2 = client.get('/api/watchlist/export?format=csv&links=1')
|
r2 = client.get('/api/watchlist/export?format=csv&links=1')
|
||||||
|
|
@ -115,10 +116,11 @@ def test_extra_fields_become_csv_columns():
|
||||||
|
|
||||||
|
|
||||||
def test_library_export_endpoint_wiring(client):
|
def test_library_export_endpoint_wiring(client):
|
||||||
|
# Robust to a shared DB that may already hold artist rows.
|
||||||
r = client.get('/api/library/artists/export?format=json&contents=1&links=1')
|
r = client.get('/api/library/artists/export?format=json&contents=1&links=1')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.data.decode().strip() == '[]' # empty test DB
|
assert isinstance(json.loads(r.data.decode()), list)
|
||||||
assert r.headers.get('X-Export-Count') == '0'
|
assert r.headers.get('X-Export-Ext') == 'json'
|
||||||
r2 = client.get('/api/library/artists/export?format=csv&contents=1')
|
r2 = client.get('/api/library/artists/export?format=csv&contents=1')
|
||||||
header = r2.data.decode().splitlines()[0]
|
header = r2.data.decode().splitlines()[0]
|
||||||
assert 'album_count' in header and 'track_count' in header
|
assert 'album_count' in header and 'track_count' in header
|
||||||
|
|
|
||||||
|
|
@ -2488,9 +2488,9 @@
|
||||||
<span class="stat-label">Artists</span>
|
<span class="stat-label">Artists</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="library-watchlist-all-btn library-export-btn" id="library-export-btn" onclick="openArtistExportModal('library')" title="Export your whole library — artists, IDs, links, optional album/track counts">
|
<button class="library-watchlist-all-btn library-export-btn" id="library-export-btn" onclick="openArtistExportModal()" title="Export artists — pick watchlist or whole library, as JSON / CSV / text">
|
||||||
<span class="watchlist-all-icon">⬇</span>
|
<span class="watchlist-all-icon">⬇</span>
|
||||||
<span class="watchlist-all-text">Export Library</span>
|
<span class="watchlist-all-text">Export</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2511,10 +2511,6 @@
|
||||||
<span class="watchlist-all-icon">👁️</span>
|
<span class="watchlist-all-icon">👁️</span>
|
||||||
<span class="watchlist-all-text">Watch All Unwatched</span>
|
<span class="watchlist-all-text">Watch All Unwatched</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="library-watchlist-all-btn watchlist-export-btn" id="watchlist-export-btn" onclick="openWatchlistExportModal()" title="Export your watchlist roster (JSON / CSV / text)">
|
|
||||||
<span class="watchlist-all-icon">⬇</span>
|
|
||||||
<span class="watchlist-all-text">Export</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metadata Source Filter -->
|
<!-- Metadata Source Filter -->
|
||||||
|
|
|
||||||
|
|
@ -9017,13 +9017,11 @@ function _arecEscAttr(s) {
|
||||||
// optional external discography links. Reuses the DB-record modal aesthetic +
|
// optional external discography links. Reuses the DB-record modal aesthetic +
|
||||||
// helpers (_jsonSyntaxHighlight / _arecCopy / _arecEsc). #export-request
|
// helpers (_jsonSyntaxHighlight / _arecCopy / _arecEsc). #export-request
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
function openWatchlistExportModal() { return openArtistExportModal('watchlist'); }
|
async function openArtistExportModal(initialScope) {
|
||||||
|
// One export modal for both rosters — pick Watchlist or Library inside.
|
||||||
async function openArtistExportModal(scope) {
|
let scope = initialScope || 'watchlist';
|
||||||
scope = scope || 'watchlist';
|
const epOf = (s) => s === 'library' ? '/api/library/artists/export' : '/api/watchlist/export';
|
||||||
const isLib = scope === 'library';
|
const fileOf = (s) => s === 'library' ? 'library_artists' : 'watchlist';
|
||||||
const endpoint = isLib ? '/api/library/artists/export' : '/api/watchlist/export';
|
|
||||||
const fileBase = isLib ? 'library_artists' : 'watchlist';
|
|
||||||
|
|
||||||
const existing = document.getElementById('wl-export-overlay');
|
const existing = document.getElementById('wl-export-overlay');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
@ -9035,10 +9033,11 @@ async function openArtistExportModal(scope) {
|
||||||
'<div class="arec-card" role="dialog" aria-label="Export artists">' +
|
'<div class="arec-card" role="dialog" aria-label="Export artists">' +
|
||||||
'<div class="arec-header">' +
|
'<div class="arec-header">' +
|
||||||
'<div class="arec-title-wrap">' +
|
'<div class="arec-title-wrap">' +
|
||||||
'<div class="arec-title"><span class="arec-dot"></span>' + (isLib ? 'Export Library' : 'Export Watchlist') + '</div>' +
|
'<div class="arec-title"><span class="arec-dot"></span>Export Artists</div>' +
|
||||||
'<div class="arec-sub">' + (isLib
|
'<div class="arec-tabs" id="wlx-scope" style="margin-top:7px;">' +
|
||||||
? 'every library artist — name, source IDs, optional links + owned counts'
|
'<button class="arec-tab' + (scope === 'watchlist' ? ' active' : '') + '" data-scope="watchlist">Watchlist</button>' +
|
||||||
: 'your watchlist artists — name, source IDs, optional links') + '</div>' +
|
'<button class="arec-tab' + (scope === 'library' ? ' active' : '') + '" data-scope="library">Library</button>' +
|
||||||
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<button class="arec-close" id="wlx-close" title="Close (Esc)">×</button>' +
|
'<button class="arec-close" id="wlx-close" title="Close (Esc)">×</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|
@ -9049,7 +9048,7 @@ async function openArtistExportModal(scope) {
|
||||||
'<button class="arec-tab" data-fmt="txt">Text</button>' +
|
'<button class="arec-tab" data-fmt="txt">Text</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<label class="wlx-opt"><input type="checkbox" id="wlx-links"> external links</label>' +
|
'<label class="wlx-opt"><input type="checkbox" id="wlx-links"> external links</label>' +
|
||||||
(isLib ? '<label class="wlx-opt"><input type="checkbox" id="wlx-contents"> library counts</label>' : '') +
|
'<label class="wlx-opt" id="wlx-contents-wrap" style="display:none;"><input type="checkbox" id="wlx-contents"> library counts</label>' +
|
||||||
'<div class="arec-actions">' +
|
'<div class="arec-actions">' +
|
||||||
'<button class="arec-btn" id="wlx-copy">Copy</button>' +
|
'<button class="arec-btn" id="wlx-copy">Copy</button>' +
|
||||||
'<button class="arec-btn" id="wlx-download">Download</button>' +
|
'<button class="arec-btn" id="wlx-download">Download</button>' +
|
||||||
|
|
@ -9063,6 +9062,17 @@ async function openArtistExportModal(scope) {
|
||||||
|
|
||||||
let fmt = 'json', links = false, contents = false, content = '';
|
let fmt = 'json', links = false, contents = false, content = '';
|
||||||
|
|
||||||
|
const applyScopeUI = () => {
|
||||||
|
// "library counts" only applies to the library roster.
|
||||||
|
document.getElementById('wlx-contents-wrap').style.display = (scope === 'library') ? '' : 'none';
|
||||||
|
if (scope !== 'library') {
|
||||||
|
contents = false;
|
||||||
|
const cb = document.getElementById('wlx-contents');
|
||||||
|
if (cb) cb.checked = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
applyScopeUI();
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
overlay.classList.remove('visible');
|
overlay.classList.remove('visible');
|
||||||
document.removeEventListener('keydown', onKey);
|
document.removeEventListener('keydown', onKey);
|
||||||
|
|
@ -9077,12 +9087,13 @@ async function openArtistExportModal(scope) {
|
||||||
const body = document.getElementById('wlx-body');
|
const body = document.getElementById('wlx-body');
|
||||||
body.innerHTML = '<div class="arec-loading">Building export…</div>';
|
body.innerHTML = '<div class="arec-loading">Building export…</div>';
|
||||||
try {
|
try {
|
||||||
const res = await fetch(endpoint + '?format=' + fmt + '&links=' + (links ? '1' : '0')
|
const res = await fetch(epOf(scope) + '?format=' + fmt + '&links=' + (links ? '1' : '0')
|
||||||
+ (isLib && contents ? '&contents=1' : ''));
|
+ (scope === 'library' && contents ? '&contents=1' : ''));
|
||||||
content = await res.text();
|
content = await res.text();
|
||||||
const count = res.headers.get('X-Export-Count') || '?';
|
const count = res.headers.get('X-Export-Count') || '?';
|
||||||
document.getElementById('wlx-footer').innerHTML =
|
document.getElementById('wlx-footer').innerHTML =
|
||||||
'<span><b>' + count + '</b> artists</span><span class="arec-id">' + fmt.toUpperCase() + '</span>';
|
'<span><b>' + count + '</b> ' + (scope === 'library' ? 'library' : 'watchlist') + ' artists</span>' +
|
||||||
|
'<span class="arec-id">' + fmt.toUpperCase() + '</span>';
|
||||||
if (fmt === 'json') {
|
if (fmt === 'json') {
|
||||||
let parsed; try { parsed = JSON.parse(content || '[]'); } catch (e) { parsed = []; }
|
let parsed; try { parsed = JSON.parse(content || '[]'); } catch (e) { parsed = []; }
|
||||||
body.innerHTML = '<pre class="arec-code">' + _jsonSyntaxHighlight(parsed) + '</pre>';
|
body.innerHTML = '<pre class="arec-code">' + _jsonSyntaxHighlight(parsed) + '</pre>';
|
||||||
|
|
@ -9094,6 +9105,16 @@ async function openArtistExportModal(scope) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
overlay.querySelectorAll('#wlx-scope .arec-tab').forEach(t => {
|
||||||
|
t.onclick = () => {
|
||||||
|
if (t.dataset.scope === scope) return;
|
||||||
|
overlay.querySelectorAll('#wlx-scope .arec-tab').forEach(x => x.classList.remove('active'));
|
||||||
|
t.classList.add('active');
|
||||||
|
scope = t.dataset.scope;
|
||||||
|
applyScopeUI();
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
});
|
||||||
overlay.querySelectorAll('#wlx-format .arec-tab').forEach(t => {
|
overlay.querySelectorAll('#wlx-format .arec-tab').forEach(t => {
|
||||||
t.onclick = () => {
|
t.onclick = () => {
|
||||||
overlay.querySelectorAll('#wlx-format .arec-tab').forEach(x => x.classList.remove('active'));
|
overlay.querySelectorAll('#wlx-format .arec-tab').forEach(x => x.classList.remove('active'));
|
||||||
|
|
@ -9103,8 +9124,7 @@ async function openArtistExportModal(scope) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
document.getElementById('wlx-links').addEventListener('change', (e) => { links = e.target.checked; refresh(); });
|
document.getElementById('wlx-links').addEventListener('change', (e) => { links = e.target.checked; refresh(); });
|
||||||
const _contentsEl = document.getElementById('wlx-contents');
|
document.getElementById('wlx-contents').addEventListener('change', (e) => { contents = e.target.checked; refresh(); });
|
||||||
if (_contentsEl) _contentsEl.addEventListener('change', (e) => { contents = e.target.checked; refresh(); });
|
|
||||||
document.getElementById('wlx-copy').onclick = () => _arecCopy(content, 'Export copied');
|
document.getElementById('wlx-copy').onclick = () => _arecCopy(content, 'Export copied');
|
||||||
document.getElementById('wlx-download').onclick = () => {
|
document.getElementById('wlx-download').onclick = () => {
|
||||||
const ext = fmt;
|
const ext = fmt;
|
||||||
|
|
@ -9112,10 +9132,10 @@ async function openArtistExportModal(scope) {
|
||||||
const blob = new Blob([content || ''], { type: mime });
|
const blob = new Blob([content || ''], { type: mime });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url; a.download = fileBase + '_export.' + ext;
|
a.href = url; a.download = fileOf(scope) + '_export.' + ext;
|
||||||
document.body.appendChild(a); a.click(); a.remove();
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
if (typeof showToast === 'function') showToast('Saved ' + fileBase + '_export.' + ext, 'success');
|
if (typeof showToast === 'function') showToast('Saved ' + fileOf(scope) + '_export.' + ext, 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue