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):
|
||||
# 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')
|
||||
assert r.status_code == 200
|
||||
assert r.data.decode().strip() == '[]'
|
||||
assert r.headers.get('X-Export-Count') == '0'
|
||||
assert isinstance(json.loads(r.data.decode()), list)
|
||||
assert r.headers.get('X-Export-Ext') == 'json'
|
||||
|
||||
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):
|
||||
# Robust to a shared DB that may already hold artist rows.
|
||||
r = client.get('/api/library/artists/export?format=json&contents=1&links=1')
|
||||
assert r.status_code == 200
|
||||
assert r.data.decode().strip() == '[]' # empty test DB
|
||||
assert r.headers.get('X-Export-Count') == '0'
|
||||
assert isinstance(json.loads(r.data.decode()), list)
|
||||
assert r.headers.get('X-Export-Ext') == 'json'
|
||||
r2 = client.get('/api/library/artists/export?format=csv&contents=1')
|
||||
header = r2.data.decode().splitlines()[0]
|
||||
assert 'album_count' in header and 'track_count' in header
|
||||
|
|
|
|||
|
|
@ -2488,9 +2488,9 @@
|
|||
<span class="stat-label">Artists</span>
|
||||
</span>
|
||||
</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-text">Export Library</span>
|
||||
<span class="watchlist-all-text">Export</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -2511,10 +2511,6 @@
|
|||
<span class="watchlist-all-icon">👁️</span>
|
||||
<span class="watchlist-all-text">Watch All Unwatched</span>
|
||||
</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>
|
||||
|
||||
<!-- Metadata Source Filter -->
|
||||
|
|
|
|||
|
|
@ -9017,13 +9017,11 @@ function _arecEscAttr(s) {
|
|||
// optional external discography links. Reuses the DB-record modal aesthetic +
|
||||
// helpers (_jsonSyntaxHighlight / _arecCopy / _arecEsc). #export-request
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
function openWatchlistExportModal() { return openArtistExportModal('watchlist'); }
|
||||
|
||||
async function openArtistExportModal(scope) {
|
||||
scope = scope || 'watchlist';
|
||||
const isLib = scope === 'library';
|
||||
const endpoint = isLib ? '/api/library/artists/export' : '/api/watchlist/export';
|
||||
const fileBase = isLib ? 'library_artists' : 'watchlist';
|
||||
async function openArtistExportModal(initialScope) {
|
||||
// One export modal for both rosters — pick Watchlist or Library inside.
|
||||
let scope = initialScope || 'watchlist';
|
||||
const epOf = (s) => s === 'library' ? '/api/library/artists/export' : '/api/watchlist/export';
|
||||
const fileOf = (s) => s === 'library' ? 'library_artists' : 'watchlist';
|
||||
|
||||
const existing = document.getElementById('wl-export-overlay');
|
||||
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-header">' +
|
||||
'<div class="arec-title-wrap">' +
|
||||
'<div class="arec-title"><span class="arec-dot"></span>' + (isLib ? 'Export Library' : 'Export Watchlist') + '</div>' +
|
||||
'<div class="arec-sub">' + (isLib
|
||||
? 'every library artist — name, source IDs, optional links + owned counts'
|
||||
: 'your watchlist artists — name, source IDs, optional links') + '</div>' +
|
||||
'<div class="arec-title"><span class="arec-dot"></span>Export Artists</div>' +
|
||||
'<div class="arec-tabs" id="wlx-scope" style="margin-top:7px;">' +
|
||||
'<button class="arec-tab' + (scope === 'watchlist' ? ' active' : '') + '" data-scope="watchlist">Watchlist</button>' +
|
||||
'<button class="arec-tab' + (scope === 'library' ? ' active' : '') + '" data-scope="library">Library</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<button class="arec-close" id="wlx-close" title="Close (Esc)">×</button>' +
|
||||
'</div>' +
|
||||
|
|
@ -9049,7 +9048,7 @@ async function openArtistExportModal(scope) {
|
|||
'<button class="arec-tab" data-fmt="txt">Text</button>' +
|
||||
'</div>' +
|
||||
'<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">' +
|
||||
'<button class="arec-btn" id="wlx-copy">Copy</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 = '';
|
||||
|
||||
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 = () => {
|
||||
overlay.classList.remove('visible');
|
||||
document.removeEventListener('keydown', onKey);
|
||||
|
|
@ -9077,12 +9087,13 @@ async function openArtistExportModal(scope) {
|
|||
const body = document.getElementById('wlx-body');
|
||||
body.innerHTML = '<div class="arec-loading">Building export…</div>';
|
||||
try {
|
||||
const res = await fetch(endpoint + '?format=' + fmt + '&links=' + (links ? '1' : '0')
|
||||
+ (isLib && contents ? '&contents=1' : ''));
|
||||
const res = await fetch(epOf(scope) + '?format=' + fmt + '&links=' + (links ? '1' : '0')
|
||||
+ (scope === 'library' && contents ? '&contents=1' : ''));
|
||||
content = await res.text();
|
||||
const count = res.headers.get('X-Export-Count') || '?';
|
||||
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') {
|
||||
let parsed; try { parsed = JSON.parse(content || '[]'); } catch (e) { parsed = []; }
|
||||
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 => {
|
||||
t.onclick = () => {
|
||||
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(); });
|
||||
const _contentsEl = document.getElementById('wlx-contents');
|
||||
if (_contentsEl) _contentsEl.addEventListener('change', (e) => { contents = e.target.checked; refresh(); });
|
||||
document.getElementById('wlx-contents').addEventListener('change', (e) => { contents = e.target.checked; refresh(); });
|
||||
document.getElementById('wlx-copy').onclick = () => _arecCopy(content, 'Export copied');
|
||||
document.getElementById('wlx-download').onclick = () => {
|
||||
const ext = fmt;
|
||||
|
|
@ -9112,10 +9132,10 @@ async function openArtistExportModal(scope) {
|
|||
const blob = new Blob([content || ''], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
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();
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue