// panes/items.js — CRUD against /api/items with search, bulk select, export/import. import { registerPane } from '../app.js'; import { jget, jpost, jdelete, escapeHtml } from '../lib/api.js'; import { ok, err, copyToClipboard, fmtJSON } from '../lib/ui.js'; const TPL = `
Items
Backend-managed records · /api/items.

New item

Bulk actions

0 selected

Records

loading…
`; let all = []; const selected = new Set(); function row(it, checked) { return `
#${it.id}
${escapeHtml(it.name)}
${escapeHtml(it.description || '')}
delete
`; } function render(host) { const q = host.querySelector('#items-search').value.trim().toLowerCase(); const filtered = q ? all.filter(it => (it.name + ' ' + (it.description || '')).toLowerCase().includes(q)) : all; host.querySelector('#items-count').textContent = `${filtered.length}/${all.length}`; host.querySelector('#items-list').innerHTML = filtered.length ? filtered.map(it => row(it, selected.has(it.id))).join('') : '
no items match
'; host.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', async () => { if (!confirm(`Delete item #${b.dataset.del}?`)) return; try { await jdelete('/api/items/' + b.dataset.del); selected.delete(+b.dataset.del); await load(host); ok('deleted'); } catch (e) { err(e.message); } })); host.querySelectorAll('[data-copy]').forEach(b => b.addEventListener('click', () => { const it = all.find(x => x.id === +b.dataset.copy); if (it) copyToClipboard(fmtJSON(it), 'copied'); })); host.querySelectorAll('[data-sel]').forEach(c => c.addEventListener('change', () => { const id = +c.dataset.sel; if (c.checked) selected.add(id); else selected.delete(id); updateSel(host); })); } function updateSel(host) { host.querySelector('#items-sel-count').textContent = `${selected.size} selected`; host.querySelector('#items-sel-del').disabled = selected.size === 0; } async function load(host) { try { const j = await jget('/api/items'); all = j.items || []; // prune stale selections const ids = new Set(all.map(x => x.id)); for (const id of [...selected]) if (!ids.has(id)) selected.delete(id); render(host); updateSel(host); } catch (e) { host.querySelector('#items-list').innerHTML = `
${escapeHtml(e.message)}
`; } } registerPane('items', { label: 'Items', init(host) { host.innerHTML = TPL; host.querySelector('#items-refresh').addEventListener('click', () => load(host)); host.querySelector('#items-search').addEventListener('input', () => render(host)); host.querySelector('#items-form').addEventListener('submit', async (e) => { e.preventDefault(); const name = host.querySelector('#items-name').value.trim(); const description = host.querySelector('#items-desc').value.trim(); if (!name) return; try { await jpost('/api/items', { name, description }); host.querySelector('#items-form').reset(); ok('added'); await load(host); } catch (e) { err(e.message); } }); host.querySelector('#items-sel-all').addEventListener('click', () => { all.forEach(x => selected.add(x.id)); render(host); updateSel(host); }); host.querySelector('#items-sel-none').addEventListener('click', () => { selected.clear(); render(host); updateSel(host); }); host.querySelector('#items-sel-del').addEventListener('click', async () => { const ids = [...selected]; if (!ids.length) return; if (!confirm(`Delete ${ids.length} item(s)?`)) return; for (const id of ids) { try { await jdelete('/api/items/' + id); } catch (e) { err(`#${id}: ${e.message}`); } } selected.clear(); ok(`deleted ${ids.length}`); await load(host); }); host.querySelector('#items-export').addEventListener('click', () => { const blob = new Blob([JSON.stringify(all, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `cxwebapp-items-${new Date().toISOString().slice(0, 10)}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }); host.querySelector('#items-import').addEventListener('click', () => host.querySelector('#items-file').click()); host.querySelector('#items-file').addEventListener('change', async (e) => { const f = e.target.files?.[0]; if (!f) return; try { const text = await f.text(); const arr = JSON.parse(text); if (!Array.isArray(arr)) throw new Error('expected JSON array'); let n = 0; for (const it of arr) { if (!it?.name) continue; try { await jpost('/api/items', { name: it.name, description: it.description || '' }); n++; } catch {} } ok(`imported ${n}/${arr.length}`); await load(host); } catch (err2) { err('import: ' + err2.message); } e.target.value = ''; }); load(host); }, refresh: load, });