diff --git a/templates/admin/metadata_backfill.html b/templates/admin/metadata_backfill.html index 84459cd..41bd18e 100644 --- a/templates/admin/metadata_backfill.html +++ b/templates/admin/metadata_backfill.html @@ -275,7 +275,13 @@ async function _fetchTypeahead(input, fieldKind) { return; } + // Args go in data-* attributes (not inline onclick) to avoid the quote + // collision when location names contain characters JSON.stringify quotes + // (e.g. anything with spaces/punctuation — basically every real name). + // _esc() escapes for HTML attribute context (entities for <>&"), then + // the browser decodes them when reading the dataset value. dropdown.innerHTML = items.map((it, idx) => { + const cid = _esc(input.dataset.clusterId); if (it.kind === 'match') { const m = it.payload; const scoreBadge = m.score >= 0.99 @@ -291,16 +297,24 @@ async function _fetchTypeahead(input, fieldKind) { } const metaLine = meta.length ? `
${meta.join(' · ')}
` : ''; return ``; } return ``; @@ -308,6 +322,19 @@ async function _fetchTypeahead(input, fieldKind) { dropdown.classList.remove('hidden'); } +// Trampoline — reads the click target's data-* attributes and forwards +// to onTypeaheadPick. Keeps the inline onclick attribute free of any +// string interpolation that could collide with HTML quoting. +function _typeaheadPickFromButton(btn) { + onTypeaheadPick( + null, + btn.dataset.cid, + btn.dataset.fieldKind, + btn.dataset.entityId || '', + btn.dataset.entityName || '' + ); +} + function onTypeaheadPick(event, clusterId, fieldKind, entityId, name) { // entityId is empty string for "create new", or a UUID for matched existing. const inputs = document.querySelectorAll(`input[data-cluster-id="${clusterId}"]`);