feat: Add functionality to move selected series between cases with a new form and UI enhancements

This commit is contained in:
Ross
2026-04-27 22:16:06 +01:00
parent 3772846e55
commit 70ea3788ca
4 changed files with 256 additions and 56 deletions
+153 -54
View File
@@ -417,50 +417,80 @@
Manage Series
</button>
</summary>
<a href="{% url 'atlas:series_id_create' pk=case.pk %}">Create and add new series</a><br />
<button class="btn btn-sm btn-outline-secondary mb-1" hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
title="order dicom by slice location"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="This will reorder all case series based upon slice location"
>
Order dicoms by slice location
</button>
<button class="btn btn-sm btn-outline-primary mb-1" hx-post="{% url 'atlas:combine_series' %}"
title="merge series"
hx-include="[name='series-ids']"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="This will merge all selected series into one"
>
Merge selected series
</button>
<button class="btn btn-sm btn-outline-secondary mb-1" hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
title="merge series"
hx-include="[name='series-ids']"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="This will use the dicom date as the series description"
>
Use dates as description
</button>
<button hx-post="{% url 'atlas:remove_selected_series_from_case' case.pk %}"
title="remove selected series from this case"
hx-include="[name='series-ids']:checked"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to remove the selected series from this case? They will not be deleted from the database."
class="btn btn-warning mt-2"
type="button"
>
Remove selected series from case
</button>
<button type="button"
class="btn btn-secondary mt-2"
data-bs-toggle="modal"
data-bs-target="#reorderSeriesModal">
Reorder series
</button>
<div class="series-actions-body">
<a class="btn btn-sm btn-outline-secondary text-start" href="{% url 'atlas:series_id_create' pk=case.pk %}">Create and add new series</a>
<div class="series-selection-toolbar d-flex flex-wrap gap-2">
<button type="button" class="btn btn-sm btn-outline-primary" id="series-select-all-btn">Select all</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="series-deselect-all-btn">Deselect all</button>
</div>
<div class="series-actions-grid d-flex flex-column gap-2">
<button class="btn btn-sm btn-outline-secondary text-start" hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
title="order dicom by slice location"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="This will reorder all case series based upon slice location"
>
Order dicoms by slice location
</button>
<button class="btn btn-sm btn-outline-primary text-start" hx-post="{% url 'atlas:combine_series' %}"
title="merge series"
hx-include="[name='series-ids']"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="This will merge all selected series into one"
>
Merge selected series
</button>
<button class="btn btn-sm btn-outline-secondary text-start" hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
title="use dicom dates as series descriptions"
hx-include="[name='series-ids']"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="This will use the dicom date as the series description"
>
Use dates as description
</button>
<button hx-post="{% url 'atlas:remove_selected_series_from_case' case.pk %}"
title="remove selected series from this case"
hx-include="[name='series-ids']:checked"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to remove the selected series from this case? They will not be deleted from the database."
class="btn btn-sm btn-warning text-start"
type="button"
>
Remove selected series from case
</button>
<button type="button"
class="btn btn-sm btn-secondary text-start"
data-bs-toggle="modal"
data-bs-target="#reorderSeriesModal">
Reorder series
</button>
</div>
<div class="series-move-panel border rounded p-2">
<div class="small fw-semibold mb-2">Move selected series to another case</div>
{% if move_series_form %}
<form id="move-selected-series-form" class="d-flex flex-column gap-2">
{{ move_series_form.destination_case }}
<button type="button"
class="btn btn-sm btn-outline-primary text-start"
hx-post="{% url 'atlas:move_selected_series_to_case' case.pk %}"
hx-include="[name='series-ids']:checked, #move-selected-series-form [name='destination_case']"
hx-target="#series-action-results"
hx-swap="innerHTML"
hx-confirm="Move selected series to the chosen destination case?">
Move selected series
</button>
</form>
{% else %}
<div class="text-muted small">Case move options are unavailable in this view.</div>
{% endif %}
</div>
</div>
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
<span id="series-action-results" ></span>
</div>
@@ -901,32 +931,80 @@
const detailsElement = document.getElementById("series-actions");
const buttons = document.querySelectorAll(".select-series-btn");
const seriesCheckboxes = document.querySelectorAll("input[type='checkbox'][name='series-ids']");
const selectAllBtn = document.getElementById("series-select-all-btn");
const deselectAllBtn = document.getElementById("series-deselect-all-btn");
function toggleCheckboxes() {
const isOpen = detailsElement.hasAttribute("open");
buttons.forEach(button => {
const isOpen = detailsElement && detailsElement.hasAttribute("open");
buttons.forEach((button) => {
button.style.display = isOpen ? "inline-block" : "none";
});
}
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
function syncSeriesSelectionUi() {
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
const parent = btn.closest(".series-block");
if (!parent) {
return;
}
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
checkbox.checked = !checkbox.checked;
if (!checkbox) {
return;
}
btn.classList.toggle("btn-primary", checkbox.checked);
btn.classList.toggle("btn-outline-primary", !checkbox.checked);
btn.textContent = checkbox.checked ? "Selected" : "Select";
// Highlight the series-block when selected
if (checkbox.checked) {
parent.classList.add("highlight-series");
} else {
parent.classList.remove("highlight-series");
}
});
const selectedCount = Array.from(seriesCheckboxes).filter((checkbox) => checkbox.checked).length;
if (selectAllBtn) {
selectAllBtn.disabled = selectedCount === seriesCheckboxes.length && seriesCheckboxes.length > 0;
}
if (deselectAllBtn) {
deselectAllBtn.disabled = selectedCount === 0;
}
}
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
const parent = btn.closest(".series-block");
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
checkbox.checked = !checkbox.checked;
syncSeriesSelectionUi();
});
});
seriesCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", syncSeriesSelectionUi);
});
if (selectAllBtn) {
selectAllBtn.addEventListener("click", function() {
seriesCheckboxes.forEach((checkbox) => {
checkbox.checked = true;
});
syncSeriesSelectionUi();
});
}
if (deselectAllBtn) {
deselectAllBtn.addEventListener("click", function() {
seriesCheckboxes.forEach((checkbox) => {
checkbox.checked = false;
});
syncSeriesSelectionUi();
});
}
// Initial state
toggleCheckboxes();
syncSeriesSelectionUi();
// Show the alert when a result is received
@@ -942,7 +1020,9 @@
observer.observe(resultsSpan, { childList: true, subtree: true });
// Listen for toggle events on the <details> element
detailsElement.addEventListener("toggle", toggleCheckboxes);
if (detailsElement) {
detailsElement.addEventListener("toggle", toggleCheckboxes);
}
const modal = new bootstrap.Modal(document.getElementById("findingModal"));
const modalBody = document.getElementById("findingModalBody");
@@ -1231,12 +1311,31 @@
background: rgba(255,255,255,0.02);
margin-top: .5rem;
}
.series-actions-body {
display: flex;
flex-direction: column;
gap: .5rem;
}
.series-selection-toolbar {
border-top: 1px solid rgba(0,0,0,0.08);
border-bottom: 1px solid rgba(0,0,0,0.08);
padding: .5rem 0;
}
.series-actions-grid .btn,
.series-actions-body > a.btn,
.series-move-panel .btn {
width: 100%;
}
.series-move-panel {
background: rgba(255,255,255,0.03);
}
.series-move-panel .case-details {
margin-bottom: .5rem;
}
.series-actions summary { list-style: none; }
.series-actions summary::-webkit-details-marker { display:none; }
.series-actions .btn { vertical-align: middle; }
.series-actions > a, .series-actions > button { width: 100%; text-align: left; }
.series-actions > button.btn.btn-warning,
.series-actions > button.btn.btn-secondary { width: auto; align-self: flex-start; }
.series-actions .btn { text-align: left; }
@media (prefers-color-scheme: dark) {
.series-actions { background: rgba(255,255,255,0.02); border-color: rgba(255,255,255,0.04); }
}