From a792f0c7cd9d39231e285a791e7d6e2068fb381f Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 23 Mar 2026 14:18:25 +0000 Subject: [PATCH] Enhance finding and display set widgets with explicit case filtering and badge indicators --- atlas/static/atlas/js/markup_inserter.js | 100 ++---------------- .../partials/displayset_search_widget.html | 66 ++++++++++++ .../atlas/partials/finding_search_widget.html | 42 +++++++- 3 files changed, 115 insertions(+), 93 deletions(-) diff --git a/atlas/static/atlas/js/markup_inserter.js b/atlas/static/atlas/js/markup_inserter.js index 3e4cbb4f..54248101 100644 --- a/atlas/static/atlas/js/markup_inserter.js +++ b/atlas/static/atlas/js/markup_inserter.js @@ -39,9 +39,8 @@ toolbar.querySelectorAll('.markup-insert-button').forEach(function(btn){ btn.addEventListener('click', function(){ const t = btn.dataset.type; - // determine a sensible default case id from the DOM context - const detected = detectCurrentCase(textarea); - const detectedCase = detected && detected.id ? detected.id : null; + // Respect explicit dataset provided by the widget or page meta only + var detectedCase = textarea && textarea.dataset && textarea.dataset.currentCase ? textarea.dataset.currentCase : (document.getElementById('current-case-meta') ? document.getElementById('current-case-meta').getAttribute('data-current-case') : null); if(t === 'case'){ openCasePicker(textarea); } else if(t === 'finding'){ @@ -198,25 +197,9 @@ Object.assign(overlay.style, {position:'fixed',left:0,top:0,right:0,bottom:0,background:'rgba(0,0,0,0.4)',zIndex:1050,display:'flex',alignItems:'center',justifyContent:'center'}); const dialog = document.createElement('div'); dialog.className='markup-case-picker-dialog card'; Object.assign(dialog.style, {width:'min(900px,95%)',maxHeight:'80vh',overflow:'auto'}); const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center'; header.innerHTML = 'Select finding'; - var detectedF = detectCurrentCase(textarea) || {}; + // Determine case filter from explicit dataset on the textarea or provided caseId + var detectedF = (textarea && textarea.dataset && textarea.dataset.currentCase) ? { id: textarea.dataset.currentCase, title: textarea.dataset.currentCaseTitle } : {}; var currentCaseF = caseId || detectedF.id || null; - var currentTitleF = detectedF.title || null; - var badgeContainerF = document.createElement('div'); badgeContainerF.className = 'd-flex align-items-center'; - var badgeTextF = document.createElement('span'); badgeTextF.className = 'badge bg-secondary ms-2'; badgeTextF.style.marginLeft='8px'; - function updateBadgeF(){ - if(currentCaseF){ - badgeTextF.textContent = 'Filtering: ' + (currentTitleF || ('#'+currentCaseF)); - removeBtnF.style.display = ''; - } else { - badgeTextF.textContent = 'No case filter'; - removeBtnF.style.display = 'none'; - } - } - var removeBtnF = document.createElement('button'); removeBtnF.type='button'; removeBtnF.className='btn btn-sm btn-outline-light ms-2'; removeBtnF.style.padding='0 .4rem'; removeBtnF.textContent='✕'; - removeBtnF.addEventListener('click', function(){ currentCaseF = null; currentTitleF = null; updateBadgeF(); fetchInitialFindings(); }); - badgeContainerF.appendChild(badgeTextF); badgeContainerF.appendChild(removeBtnF); - updateBadgeF(); - header.appendChild(badgeContainerF); const closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn-close'; closeBtn.setAttribute('aria-label','Close'); closeBtn.addEventListener('click', function(){ document.body.removeChild(overlay); }); header.appendChild(closeBtn); const body = document.createElement('div'); body.className='card-body'; const input = document.createElement('input'); input.type='search'; input.className='form-control mb-2'; input.placeholder='Type to search findings...'; @@ -242,25 +225,9 @@ const dialog = document.createElement('div'); dialog.className='markup-case-picker-dialog card'; Object.assign(dialog.style, {width:'min(900px,95%)',maxHeight:'80vh',overflow:'auto'}); const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center'; header.innerHTML = 'Select display set'; - var detectedD = detectCurrentCase(textarea) || {}; + // Determine case filter from explicit dataset on the textarea or provided caseId + var detectedD = (textarea && textarea.dataset && textarea.dataset.currentCase) ? { id: textarea.dataset.currentCase, title: textarea.dataset.currentCaseTitle } : {}; var currentCaseD = caseId || detectedD.id || null; - var currentTitleD = detectedD.title || null; - var badgeContainerD = document.createElement('div'); badgeContainerD.className = 'd-flex align-items-center'; - var badgeTextD = document.createElement('span'); badgeTextD.className = 'badge bg-secondary ms-2'; badgeTextD.style.marginLeft='8px'; - function updateBadgeD(){ - if(currentCaseD){ - badgeTextD.textContent = 'Filtering: ' + (currentTitleD || ('#'+currentCaseD)); - removeBtnD.style.display = ''; - } else { - badgeTextD.textContent = 'No case filter'; - removeBtnD.style.display = 'none'; - } - } - var removeBtnD = document.createElement('button'); removeBtnD.type='button'; removeBtnD.className='btn btn-sm btn-outline-light ms-2'; removeBtnD.style.padding='0 .4rem'; removeBtnD.textContent='✕'; - removeBtnD.addEventListener('click', function(){ currentCaseD = null; currentTitleD = null; updateBadgeD(); fetchInitialDisplaysets(); }); - badgeContainerD.appendChild(badgeTextD); badgeContainerD.appendChild(removeBtnD); - updateBadgeD(); - header.appendChild(badgeContainerD); const closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn-close'; closeBtn.setAttribute('aria-label','Close'); closeBtn.addEventListener('click', function(){ document.body.removeChild(overlay); }); header.appendChild(closeBtn); const body = document.createElement('div'); body.className='card-body'; const input = document.createElement('input'); input.type='search'; input.className='form-control mb-2'; input.placeholder='Type to search display sets...'; @@ -277,58 +244,7 @@ }catch(err){ console.error('openDisplaysetPicker error', err); } } - function detectCurrentCase(contextEl){ - try{ - var result = { id: null, title: null }; - var root = contextEl && contextEl.closest ? contextEl.closest('form,body') : document; - var inp = root.querySelector('input[name="case"]') || root.querySelector('input[name="case_id"]') || document.querySelector('input[name="case"]') || document.querySelector('input[name="case_id"]'); - if(inp && inp.value) result.id = inp.value; - - // Look for explicit metadata element - var meta = document.querySelector('#current-case-meta') || document.querySelector('[data-current-case]'); - if(meta){ - if(!result.id) result.id = meta.getAttribute('data-current-case') || meta.getAttribute('data-case-pk') || meta.value || null; - result.title = meta.getAttribute('data-current-case-title') || meta.getAttribute('data-case-title') || null; - } - - // Fallback: look for ancestor attributes - if(!result.id){ - var anc = contextEl; - while(anc){ - if(anc.getAttribute && (anc.getAttribute('data-case-pk') || anc.getAttribute('data-case-id'))){ - result.id = anc.getAttribute('data-case-pk') || anc.getAttribute('data-case-id'); - break; - } - anc = anc.parentElement; - } - } - - // Fallback: check URL query string for ?case= or ?case_id= - if(!result.id){ - try{ - var params = new URLSearchParams(window.location.search); - if(params.has('case')) result.id = params.get('case'); - else if(params.has('case_id')) result.id = params.get('case_id'); - }catch(e){} - } - - // Fallback: try to parse a /case// segment from the pathname - if(!result.id){ - try{ - var m = window.location.pathname.match(/\/case\/(\d+)(?:\/|$)/i) || window.location.pathname.match(/\/atlas\/case\/(\d+)(?:\/|$)/i); - if(m) result.id = m[1]; - }catch(e){} - } - - // If there's an H2 like 'Display Sets: TITLE' try to use it as title - if(!result.title){ - var h2 = document.querySelector('h2'); - if(h2 && /Display Sets:/i.test(h2.textContent || '')){ - result.title = (h2.textContent || '').replace(/Display Sets:\s*/i, '').trim(); - } - } - return result; - }catch(e){ return { id: null, title: null }; } - } + // Simplified detection: rely on explicit dataset attributes set by the widget + // When a textarea includes `data-current-case` and optionally `data-current-case-title`, that will be used as the default filter. })(window); diff --git a/atlas/templates/atlas/partials/displayset_search_widget.html b/atlas/templates/atlas/partials/displayset_search_widget.html index bc92889d..5267ad86 100644 --- a/atlas/templates/atlas/partials/displayset_search_widget.html +++ b/atlas/templates/atlas/partials/displayset_search_widget.html @@ -13,6 +13,13 @@ hx-trigger="keyup changed delay:400ms" autocomplete="off"> +
+ + {% if case %}Filtering: {{ case.title|escape }}{% endif %} + + +
+
{% include 'atlas/partials/_displayset_search_results.html' with displaysets=displaysets case=case %}
@@ -46,3 +53,62 @@ } catch(e) { console.error('displayset search widget init error', e); } })(); + + diff --git a/atlas/templates/atlas/partials/finding_search_widget.html b/atlas/templates/atlas/partials/finding_search_widget.html index 2e34d50d..2eabf165 100644 --- a/atlas/templates/atlas/partials/finding_search_widget.html +++ b/atlas/templates/atlas/partials/finding_search_widget.html @@ -10,6 +10,13 @@ hx-trigger="keyup changed delay:400ms" autocomplete="off"> +
+ + {% if case %}Filtering: {{ case.title|escape }}{% endif %} + + +
+
{% include 'atlas/partials/_finding_search_results.html' with findings=findings case=case %}
@@ -22,12 +29,45 @@ var input = document.getElementById('{{ input_id|default:"finding-search-input" }}'); if (!input) return; var timer = null; + + // If server provided a case via inclusion, ensure widget sets dataset and nearby textarea dataset + var meta = document.getElementById('current-case-meta'); + var casePk = meta ? meta.getAttribute('data-current-case') : null; + var caseTitle = meta ? meta.getAttribute('data-current-case-title') : ''; + var badge = document.getElementById('{{ input_id }}-case-badge'); + var clearBtn = document.getElementById('{{ input_id }}-clear-case'); + + function applyCaseToInputs(pk, title){ + if(pk){ + input.dataset.currentCase = pk; + if(title) input.dataset.currentCaseTitle = title; + var container = input.closest('form,div') || document.body; + var ta = container.querySelector('textarea'); + if(ta){ ta.dataset.currentCase = pk; if(title) ta.dataset.currentCaseTitle = title; } + if(badge){ badge.style.display = 'inline-block'; badge.textContent = 'Filtering: ' + (title || ('#' + pk)); } + if(clearBtn) clearBtn.style.display = 'inline-block'; + } else { + delete input.dataset.currentCase; delete input.dataset.currentCaseTitle; + var container = input.closest('form,div') || document.body; + var ta = container.querySelector('textarea'); + if(ta){ delete ta.dataset.currentCase; delete ta.dataset.currentCaseTitle; } + if(badge) badge.style.display = 'none'; + if(clearBtn) clearBtn.style.display = 'none'; + } + } + + if(casePk){ applyCaseToInputs(casePk, caseTitle); } + + if(clearBtn){ clearBtn.addEventListener('click', function(){ applyCaseToInputs(null, null); try{ input.removeAttribute('hx-vals'); }catch(e){}; input.dispatchEvent(new Event('input')); }); } + input.addEventListener('input', function(){ if (timer) clearTimeout(timer); timer = setTimeout(function(){ try { var q = encodeURIComponent(input.value || ''); - var url = "{% url 'atlas:finding_search' %}?q=" + q + ({% if case %} "&case={{ case.pk }}" {% else %} '' {% endif %}); + // Respect dataset-based case filter if present + var caseParam = input.dataset.currentCase ? '&case=' + encodeURIComponent(input.dataset.currentCase) : ''; + var url = "{% url 'atlas:finding_search' %}?q=" + q + caseParam; var target = '#{{ target_id|default:"finding-search-results" }}'; if (window.htmx && typeof window.htmx.ajax === 'function') { window.htmx.ajax('GET', url, { target: target, swap: 'innerHTML' });