Enhance finding and display set widgets with explicit case filtering and badge indicators
This commit is contained in:
@@ -39,9 +39,8 @@
|
|||||||
toolbar.querySelectorAll('.markup-insert-button').forEach(function(btn){
|
toolbar.querySelectorAll('.markup-insert-button').forEach(function(btn){
|
||||||
btn.addEventListener('click', function(){
|
btn.addEventListener('click', function(){
|
||||||
const t = btn.dataset.type;
|
const t = btn.dataset.type;
|
||||||
// determine a sensible default case id from the DOM context
|
// Respect explicit dataset provided by the widget or page meta only
|
||||||
const detected = detectCurrentCase(textarea);
|
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);
|
||||||
const detectedCase = detected && detected.id ? detected.id : null;
|
|
||||||
if(t === 'case'){
|
if(t === 'case'){
|
||||||
openCasePicker(textarea);
|
openCasePicker(textarea);
|
||||||
} else if(t === 'finding'){
|
} 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'});
|
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 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>';
|
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) || {};
|
// 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 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 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 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...';
|
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 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';
|
const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center';
|
||||||
header.innerHTML = '<strong>Select display set</strong>';
|
header.innerHTML = '<strong>Select display set</strong>';
|
||||||
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 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 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 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...';
|
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); }
|
}catch(err){ console.error('openDisplaysetPicker error', err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectCurrentCase(contextEl){
|
// Simplified detection: rely on explicit dataset attributes set by the widget
|
||||||
try{
|
// When a textarea includes `data-current-case` and optionally `data-current-case-title`, that will be used as the default filter.
|
||||||
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);
|
})(window);
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
hx-trigger="keyup changed delay:400ms"
|
hx-trigger="keyup changed delay:400ms"
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<span id="{{ input_id }}-case-badge" class="badge bg-secondary" style="display: {% if case %}inline-block{% else %}none{% endif %};">
|
||||||
|
{% if case %}Filtering: {{ case.title|escape }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<button type="button" id="{{ input_id }}-clear-case" class="btn btn-sm btn-outline-secondary ms-2" style="display: {% if case %}inline-block{% else %}none{% endif %};">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="{{ target_id }}" class="mt-2">
|
<div id="{{ target_id }}" class="mt-2">
|
||||||
{% include 'atlas/partials/_displayset_search_results.html' with displaysets=displaysets case=case %}
|
{% include 'atlas/partials/_displayset_search_results.html' with displaysets=displaysets case=case %}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,3 +53,62 @@
|
|||||||
} catch(e) { console.error('displayset search widget init error', e); }
|
} catch(e) { console.error('displayset search widget init error', e); }
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
try{
|
||||||
|
var input = document.getElementById('{{ input_id|default:"displayset-search-input" }}');
|
||||||
|
if(!input) return;
|
||||||
|
|
||||||
|
// Ensure a current-case-meta element exists
|
||||||
|
var meta = document.getElementById('current-case-meta');
|
||||||
|
if(!meta){
|
||||||
|
meta = document.createElement('div');
|
||||||
|
meta.id = 'current-case-meta';
|
||||||
|
meta.style.display = 'none';
|
||||||
|
var container = document.currentScript && document.currentScript.parentNode ? document.currentScript.parentNode : document.body;
|
||||||
|
container.insertBefore(meta, container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate meta from server-rendered attributes if present (existing behavior)
|
||||||
|
// If not present, do not attempt further heuristics here; widget owns filter logic.
|
||||||
|
|
||||||
|
// If meta has case info, push it into the input dataset and to a nearby textarea so the inserter can read it
|
||||||
|
var casePk = meta.getAttribute('data-current-case');
|
||||||
|
var caseTitle = 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;
|
||||||
|
// find a nearby textarea (same form or container)
|
||||||
|
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(){
|
||||||
|
// clear widget-level filter state and refresh results without case
|
||||||
|
applyCaseToInputs(null, null);
|
||||||
|
// remove any hx-vals forcing a case param
|
||||||
|
try{ input.removeAttribute('hx-vals'); }catch(e){}
|
||||||
|
input.dispatchEvent(new Event('input'));
|
||||||
|
}); }
|
||||||
|
|
||||||
|
}catch(e){ console.error('displayset widget meta init error', e); }
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
hx-trigger="keyup changed delay:400ms"
|
hx-trigger="keyup changed delay:400ms"
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<span id="{{ input_id }}-case-badge" class="badge bg-secondary" style="display: {% if case %}inline-block{% else %}none{% endif %};">
|
||||||
|
{% if case %}Filtering: {{ case.title|escape }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<button type="button" id="{{ input_id }}-clear-case" class="btn btn-sm btn-outline-secondary ms-2" style="display: {% if case %}inline-block{% else %}none{% endif %};">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="{{ target_id }}" class="mt-2">
|
<div id="{{ target_id }}" class="mt-2">
|
||||||
{% include 'atlas/partials/_finding_search_results.html' with findings=findings case=case %}
|
{% include 'atlas/partials/_finding_search_results.html' with findings=findings case=case %}
|
||||||
</div>
|
</div>
|
||||||
@@ -22,12 +29,45 @@
|
|||||||
var input = document.getElementById('{{ input_id|default:"finding-search-input" }}');
|
var input = document.getElementById('{{ input_id|default:"finding-search-input" }}');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
var timer = null;
|
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(){
|
input.addEventListener('input', function(){
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
timer = setTimeout(function(){
|
timer = setTimeout(function(){
|
||||||
try {
|
try {
|
||||||
var q = encodeURIComponent(input.value || '');
|
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" }}';
|
var target = '#{{ target_id|default:"finding-search-results" }}';
|
||||||
if (window.htmx && typeof window.htmx.ajax === 'function') {
|
if (window.htmx && typeof window.htmx.ajax === 'function') {
|
||||||
window.htmx.ajax('GET', url, { target: target, swap: 'innerHTML' });
|
window.htmx.ajax('GET', url, { target: target, swap: 'innerHTML' });
|
||||||
|
|||||||
Reference in New Issue
Block a user