Add term column

This commit is contained in:
Ian Keane 2026-06-11 17:28:14 -04:00
parent 52eb0f6189
commit dba48e90b7
2 changed files with 31 additions and 1 deletions

23
app.js
View file

@ -10,6 +10,7 @@ let allMusicians = [];
let allInstruments = []; let allInstruments = [];
let allTunings = []; let allTunings = [];
let allReferenceSites = []; let allReferenceSites = [];
let allTerms = [];
let sortCol = 'name'; let sortCol = 'name';
let sortDir = 'asc'; let sortDir = 'asc';
@ -43,6 +44,7 @@ const sourceDatalist = $('source-list');
const musicianDatalist = $('musician-list'); const musicianDatalist = $('musician-list');
const instrumentSelect = $('instrument-select'); const instrumentSelect = $('instrument-select');
const tuningDatalist = $('tuning-list'); const tuningDatalist = $('tuning-list');
const termDatalist = $('term-list');
const btnDeleteTune = $('btn-delete-tune'); const btnDeleteTune = $('btn-delete-tune');
const btnEditNotes = $('btn-edit-notes'); const btnEditNotes = $('btn-edit-notes');
const btnBulkEdit = $('btn-bulk-edit'); const btnBulkEdit = $('btn-bulk-edit');
@ -116,18 +118,20 @@ function setStatus(msg, isError = false) {
async function loadAll() { async function loadAll() {
setStatus('Loading…'); setStatus('Loading…');
try { try {
[allTunes, allSources, allMusicians, allInstruments, allTunings, allReferenceSites] = await Promise.all([ [allTunes, allSources, allMusicians, allInstruments, allTunings, allReferenceSites, allTerms] = await Promise.all([
apiFetch('/tunes/'), apiFetch('/tunes/'),
apiFetch('/sources/'), apiFetch('/sources/'),
apiFetch('/musicians/'), apiFetch('/musicians/'),
apiFetch('/instruments'), apiFetch('/instruments'),
apiFetch('/tunings/'), apiFetch('/tunings/'),
apiFetch('/reference_sites/'), apiFetch('/reference_sites/'),
apiFetch('/terms/'),
]); ]);
populateFilterOptions(); populateFilterOptions();
populateDatalist(sourceDatalist, allSources); populateDatalist(sourceDatalist, allSources);
populateDatalist(musicianDatalist, allMusicians); populateDatalist(musicianDatalist, allMusicians);
populateDatalist(tuningDatalist, allTunings); populateDatalist(tuningDatalist, allTunings);
populateDatalist(termDatalist, allTerms);
populateDatalist(refSiteDatalist, allReferenceSites); populateDatalist(refSiteDatalist, allReferenceSites);
populateInstrumentSelect(); populateInstrumentSelect();
renderTable(); renderTable();
@ -204,6 +208,12 @@ $('btn-quick-add-tuning').addEventListener('click', async () => {
}); });
}); });
$('btn-quick-add-term').addEventListener('click', async () => {
await quickAdd('/terms/', 'New term name:', allTerms, termDatalist, item => {
tuneForm.elements.inst_term.value = item.name;
});
});
$('btn-quick-add-instrument').addEventListener('click', async () => { $('btn-quick-add-instrument').addEventListener('click', async () => {
const item = await quickAdd('/instruments', 'New instrument name:', allInstruments, null); const item = await quickAdd('/instruments', 'New instrument name:', allInstruments, null);
if (item) { if (item) {
@ -231,6 +241,11 @@ function resolveTuningId(name) {
return allTunings.find(x => x.name.toLowerCase() === name.toLowerCase())?.id ?? null; return allTunings.find(x => x.name.toLowerCase() === name.toLowerCase())?.id ?? null;
} }
function resolveTermId(name) {
if (!name) return null;
return allTerms.find(x => x.name.toLowerCase() === name.toLowerCase())?.id ?? null;
}
// ── Rows: one per instrument entry ──────────────────────────── // ── Rows: one per instrument entry ────────────────────────────
// For tunes with no instrument entries, emit one row with empty instrument cols // For tunes with no instrument entries, emit one row with empty instrument cols
function buildRows(tunes) { function buildRows(tunes) {
@ -550,6 +565,7 @@ async function openEdit(tuneId, entryId) {
populateInstrumentSelect(entry?.instrument_id ?? null); populateInstrumentSelect(entry?.instrument_id ?? null);
if (entry) { if (entry) {
tuneForm.elements.inst_tuning.value = entry.tuning ?? ''; tuneForm.elements.inst_tuning.value = entry.tuning ?? '';
tuneForm.elements.inst_term.value = entry.term ?? '';
tuneForm.elements.inst_learned_from.value = entry.learned_from ?? ''; tuneForm.elements.inst_learned_from.value = entry.learned_from ?? '';
tuneForm.elements.inst_date_learned.value = entry.date_learned ?? ''; tuneForm.elements.inst_date_learned.value = entry.date_learned ?? '';
tuneForm.elements.inst_status.value = entry.status ?? ''; tuneForm.elements.inst_status.value = entry.status ?? '';
@ -708,6 +724,7 @@ async function saveTune() {
const instBody = instId ? { const instBody = instId ? {
instrument_id: instId, instrument_id: instId,
tuning_id: resolveTuningId(f.inst_tuning.value), tuning_id: resolveTuningId(f.inst_tuning.value),
term_id: resolveTermId(f.inst_term.value),
learned_from_id: resolveMusicianId(f.inst_learned_from.value), learned_from_id: resolveMusicianId(f.inst_learned_from.value),
date_learned: f.inst_date_learned.value || null, date_learned: f.inst_date_learned.value || null,
status: f.inst_status.value || null, status: f.inst_status.value || null,
@ -1432,6 +1449,7 @@ async function importCSVRows(rows) {
const tunCache = new Map(); // name.lower → id (tunings) const tunCache = new Map(); // name.lower → id (tunings)
const musCache = new Map(); // name.lower → id (musicians) const musCache = new Map(); // name.lower → id (musicians)
const instCache = new Map(); // name.lower → id (instruments) const instCache = new Map(); // name.lower → id (instruments)
const termCache = new Map(); // name.lower → id (terms)
const tuneCache = new Map(); // name.lower → id (tunes created this run) const tuneCache = new Map(); // name.lower → id (tunes created this run)
const results = { created: 0, skipped: 0, errors: [] }; const results = { created: 0, skipped: 0, errors: [] };
@ -1455,6 +1473,7 @@ async function importCSVRows(rows) {
const tuningId = await resolveOrCreate('/tunings/', allTunings, tunCache, row.tuning?.trim() || null); const tuningId = await resolveOrCreate('/tunings/', allTunings, tunCache, row.tuning?.trim() || null);
const learnedFromId = await resolveOrCreate('/musicians/', allMusicians, musCache, row.learned_from?.trim() || null); const learnedFromId = await resolveOrCreate('/musicians/', allMusicians, musCache, row.learned_from?.trim() || null);
const instId = await resolveOrCreate('/instruments', allInstruments, instCache, row.instrument?.trim() || null); const instId = await resolveOrCreate('/instruments', allInstruments, instCache, row.instrument?.trim() || null);
const termId = await resolveOrCreate('/terms/', allTerms, termCache, row.term?.trim() || null);
// Find existing tune or create new // Find existing tune or create new
const lower = tuneName.toLowerCase(); const lower = tuneName.toLowerCase();
@ -1481,6 +1500,7 @@ async function importCSVRows(rows) {
body: JSON.stringify({ body: JSON.stringify({
instrument_id: instId, instrument_id: instId,
tuning_id: tuningId, tuning_id: tuningId,
term_id: termId,
learned_from_id: learnedFromId, learned_from_id: learnedFromId,
date_learned: row.date_learned?.trim() || null, date_learned: row.date_learned?.trim() || null,
status, status,
@ -1537,6 +1557,7 @@ function attachTabAutocomplete(input, datalist) {
attachTabAutocomplete(sourceInput, sourceDatalist); attachTabAutocomplete(sourceInput, sourceDatalist);
attachTabAutocomplete(tuneForm.elements.inst_tuning, tuningDatalist); attachTabAutocomplete(tuneForm.elements.inst_tuning, tuningDatalist);
attachTabAutocomplete(tuneForm.elements.inst_term, termDatalist);
attachTabAutocomplete(tuneForm.elements.inst_learned_from, musicianDatalist); attachTabAutocomplete(tuneForm.elements.inst_learned_from, musicianDatalist);
// ── Boot ───────────────────────────────────────────────────── // ── Boot ─────────────────────────────────────────────────────

View file

@ -121,6 +121,14 @@
<button type="button" class="quick-add-btn" id="btn-quick-add-tuning" title="Add new tuning">+</button> <button type="button" class="quick-add-btn" id="btn-quick-add-tuning" title="Add new tuning">+</button>
</span> </span>
</label> </label>
<label>Term
<span class="autocomplete-row">
<input name="inst_term" type="text"
list="term-list" placeholder="— none —" autocomplete="off" />
<datalist id="term-list"></datalist>
<button type="button" class="quick-add-btn" id="btn-quick-add-term" title="Add new term">+</button>
</span>
</label>
<label>Learned from <label>Learned from
<span class="autocomplete-row"> <span class="autocomplete-row">
<input name="inst_learned_from" type="text" <input name="inst_learned_from" type="text"
@ -230,6 +238,7 @@
<tr><td>date_learned</td><td></td><td>YYYY-MM-DD</td></tr> <tr><td>date_learned</td><td></td><td>YYYY-MM-DD</td></tr>
<tr><td>status</td><td></td><td>callable / review / to_learn</td></tr> <tr><td>status</td><td></td><td>callable / review / to_learn</td></tr>
<tr><td>difficulty</td><td></td><td>easy / hard</td></tr> <tr><td>difficulty</td><td></td><td>easy / hard</td></tr>
<tr><td>term</td><td></td><td>Created if new</td></tr>
</tbody> </tbody>
</table> </table>
<p class="csv-format-note">If a tune with the same name already exists a new instrument entry is added to it. Sources, tunings, and musicians are created automatically if they dont exist.</p> <p class="csv-format-note">If a tune with the same name already exists a new instrument entry is added to it. Sources, tunings, and musicians are created automatically if they dont exist.</p>