From ba1f28ee53cfd7531a5f4b22ef064921510c90d6 Mon Sep 17 00:00:00 2001 From: serversdown Date: Fri, 15 May 2026 03:59:38 +0000 Subject: [PATCH] fix(backfill): typeahead picks broken by JSON.stringify quote collision in onclick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline onclick on each typeahead dropdown item was: onclick="onTypeaheadPick(event, 'cid', 'location', 'loc-id', ${JSON.stringify(m.name)})" For any name with spaces/punctuation (i.e. every real location name like "Area 1 - Loc 1 - 87 Jenks"), JSON.stringify emits double quotes around the value, which collide with the onclick attribute's own double quotes and terminate the attribute early. The dropdown rendered fine via .innerHTML, but the browser's HTML parser saw a broken attribute and never bound the click handler โ€” clicks on dropdown items silently did nothing. Same pattern that broke the location Remove button yesterday. Same fix: move args into data-* attributes and dispatch through a tiny trampoline that reads from this.dataset. Robust against any character in project/location names. Co-Authored-By: Claude Opus 4.7 --- templates/admin/metadata_backfill.html | 31 ++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) 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}"]`);