Enhance display set and finding pickers with case detection and filtering

This commit is contained in:
Ross
2026-03-23 13:54:44 +00:00
parent fbb2a2cc18
commit 4d6d535ca7
3 changed files with 87 additions and 6 deletions
+78 -5
View File
@@ -39,12 +39,15 @@
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;
if(t === 'case'){
openCasePicker(textarea);
} else if(t === 'finding'){
openFindingPicker(textarea);
openFindingPicker(textarea, detectedCase);
} else if(t === 'displayset'){
openDisplaysetPicker(textarea);
openDisplaysetPicker(textarea, detectedCase);
}
});
});
@@ -195,6 +198,11 @@
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 = '<strong>Select finding</strong>';
var detectedF = detectCurrentCase(textarea);
if(detectedF && detectedF.id){
var badgeF = document.createElement('span'); badgeF.className = 'badge bg-secondary ms-2'; badgeF.style.marginLeft='8px'; badgeF.textContent = 'Filtering: ' + (detectedF.title || ('#'+detectedF.id));
header.appendChild(badgeF);
}
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...';
@@ -205,7 +213,9 @@
input.addEventListener('input', function(){ if(timer) clearTimeout(timer); timer = setTimeout(function(){ var q = encodeURIComponent(input.value || ''); var url = window.location.origin + '/atlas/search/findings/?q=' + q + (caseId ? '&case=' + caseId : ''); fetch(url, {credentials:'same-origin'}).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(e){ console.error('finding picker fetch error', e); results.innerHTML = '<div class="alert alert-danger">Search failed</div>'; }); }, 300); });
results.addEventListener('click', function(e){ var item = e.target.closest('.list-group-item'); if(!item) return; var pk = item.getAttribute('data-finding-pk'); if(!pk) return; insertAtCursor(textarea, '[['+'finding:'+pk+']]' ); if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); });
overlay.tabIndex = -1; overlay.focus(); fetch(window.location.origin + '/atlas/search/findings/').then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){});
overlay.tabIndex = -1; overlay.focus();
var initFindingsUrl = window.location.origin + '/atlas/search/findings/' + (caseId ? '?case=' + caseId : '');
fetch(initFindingsUrl).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){});
}catch(err){ console.error('openFindingPicker error', err); }
}
@@ -216,7 +226,14 @@
overlay.className = 'markup-case-picker-overlay';
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 = '<strong>Select display set</strong>';
const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center';
header.innerHTML = '<strong>Select display set</strong>';
// show current case filter if present
var detected = detectCurrentCase(textarea);
if(detected && detected.id){
var badge = document.createElement('span'); badge.className = 'badge bg-secondary ms-2'; badge.style.marginLeft='8px'; badge.textContent = 'Filtering: ' + (detected.title || ('#'+detected.id));
header.appendChild(badge);
}
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...';
@@ -227,8 +244,64 @@
input.addEventListener('input', function(){ if(timer) clearTimeout(timer); timer = setTimeout(function(){ var q = encodeURIComponent(input.value || ''); var url = window.location.origin + '/atlas/search/displaysets/?q=' + q + (caseId ? '&case=' + caseId : ''); fetch(url, {credentials:'same-origin'}).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(e){ console.error('displayset picker fetch error', e); results.innerHTML = '<div class="alert alert-danger">Search failed</div>'; }); }, 300); });
results.addEventListener('click', function(e){ var item = e.target.closest('.list-group-item'); if(!item) return; var pk = item.getAttribute('data-ds-pk'); if(!pk) return; insertAtCursor(textarea, '[['+'displayset:'+pk+']]' ); if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); });
overlay.tabIndex = -1; overlay.focus(); fetch(window.location.origin + '/atlas/search/displaysets/').then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){});
overlay.tabIndex = -1; overlay.focus();
var initDisplaysetsUrl = window.location.origin + '/atlas/search/displaysets/' + (caseId ? '?case=' + caseId : '');
fetch(initDisplaysetsUrl).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){});
}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/<id>/ 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 }; }
}
})(window);
@@ -4,7 +4,12 @@
<div class="list-group-item d-flex justify-content-between align-items-center" data-ds-pk="{{ ds.pk }}">
<div class="flex-fill">
<div class="d-flex w-100 justify-content-between">
<strong class="mb-1">{{ ds.name }}</strong>
<div>
<strong class="mb-1">{{ ds.name }}</strong>
{% if ds.case %}
<div class="small text-muted">Case: <a href="{% url 'atlas:case_detail' ds.case.pk %}">{{ ds.case.title }}</a> (#{% if ds.case.pk %}{{ ds.case.pk }}{% endif %})</div>
{% endif %}
</div>
<small class="text-muted">{{ ds.pk }}</small>
</div>
<p class="mb-1 text-truncate">{{ ds.description }}</p>
@@ -1,4 +1,7 @@
<div class="displayset-search-widget">
{% if case %}
<div id="current-case-meta" data-current-case="{{ case.pk }}" data-current-case-title="{{ case.title|escape }}" style="display:none;"></div>
{% endif %}
{% with input_id=input_id|default:'displayset-search-input' target_id=target_id|default:'displayset-search-results' %}
<label for="{{ input_id }}" class="form-label">Search display sets</label>
<input id="{{ input_id }}" name="q" class="form-control" type="search"