Adding a bulk delete option to the scopes page
This commit is contained in:
1 parent
a9c4c16d4f
commit
7a15d9f7c0
5 files changed
+137
-40
No files matched your search
@@ -567,6 +567,11 @@ textarea:focus {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Bulk actions (e.g. delete-selected above a checkbox table) */
|
||||
.bulk-actions {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-bar form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
<table class="table sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if can_write_scopes %}
|
||||
<th data-sort-off><input type="checkbox" id="scopes-select-all" title="Select all"></th>
|
||||
{% endif %}
|
||||
<th>Scope</th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Model</th>
|
||||
@@ -30,9 +33,6 @@
|
||||
{% for ct in column_types %}
|
||||
<th data-sort-off>{{ ct.name }}</th>
|
||||
{% endfor %}
|
||||
{% if can_write_scopes %}
|
||||
<th data-sort-off></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -40,6 +40,9 @@
|
||||
{% set scope = row.scope %}
|
||||
{% set info = row.info %}
|
||||
<tr id="scope-row-{{ scope.id }}">
|
||||
{% if can_write_scopes %}
|
||||
<td><input type="checkbox" name="scope_ids" value="{{ scope.id }}"></td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<strong><a href="/admin/augmented-parts/{{ scope.id }}">{{ scope.description }}</a></strong>
|
||||
{% if scope.sku %}<br><span class="muted">{{ scope.sku }}</span>{% endif %}
|
||||
@@ -70,15 +73,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% if can_write_scopes %}
|
||||
<td class="row-actions">
|
||||
<button class="btn btn-danger"
|
||||
hx-delete="/scopes/{{ scope.id }}"
|
||||
hx-target="#scope-row-{{ scope.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Delete {{ scope.sku or scope.description }}?">Delete</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
|
||||
@@ -44,6 +44,50 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if can_write_scopes %}
|
||||
<div class="bulk-actions">
|
||||
<button id="delete-selected-scopes-btn" type="button" class="btn btn-danger" disabled
|
||||
hx-post="/scopes/bulk-delete"
|
||||
hx-include="[name='scope_ids']:checked, [name='description'], [name='manufacturer_id'], [name='scope_model_id'], [name='sub_part_type_id'], [name='fits_part_type_id']"
|
||||
hx-target="#scopes-table-card"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Delete selected scopes?">Delete selected</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include "_scopes_table_card.html" %}
|
||||
</main>
|
||||
|
||||
{% if can_write_scopes %}
|
||||
<script>
|
||||
(function () {
|
||||
function updateDeleteButton() {
|
||||
var count = document.querySelectorAll('input[name="scope_ids"]:checked').length;
|
||||
var btn = document.getElementById('delete-selected-scopes-btn');
|
||||
if (!btn) return;
|
||||
btn.textContent = count > 0 ? 'Delete selected (' + count + ')' : 'Delete selected';
|
||||
btn.disabled = count === 0;
|
||||
btn.setAttribute('hx-confirm', 'Delete ' + count + ' selected scope' + (count === 1 ? '' : 's') + '?');
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (e) {
|
||||
if (!e.target) return;
|
||||
|
||||
if (e.target.id === 'scopes-select-all') {
|
||||
document.querySelectorAll('input[name="scope_ids"]').forEach(function (cb) {
|
||||
cb.checked = e.target.checked;
|
||||
});
|
||||
}
|
||||
|
||||
if (e.target.id === 'scopes-select-all' || e.target.name === 'scope_ids') {
|
||||
updateDeleteButton();
|
||||
}
|
||||
});
|
||||
|
||||
// Re-run after every HTMX swap (filter results, post-delete) so the button
|
||||
// state reflects the freshly rendered checkboxes, which start unchecked.
|
||||
document.addEventListener('htmx:afterSwap', updateDeleteButton);
|
||||
}());
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
+38
-19
@@ -5,7 +5,7 @@ non-Scope part type listing the accessories that fit each scope. Supports HTMX
|
||||
search/filter over scope attributes and required accessory fits.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -119,25 +119,44 @@ async def scopes_filter(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/scopes/{augmented_part_id}")
|
||||
async def delete_scope(
|
||||
augmented_part_id: int,
|
||||
@router.post("/scopes/bulk-delete", response_class=HTMLResponse)
|
||||
async def bulk_delete_scopes(
|
||||
request: Request,
|
||||
scope_ids: list[int] = Form([]),
|
||||
description: str = Form(""),
|
||||
manufacturer_id: str = Form(""),
|
||||
scope_model_id: str = Form(""),
|
||||
sub_part_type_id: str = Form(""),
|
||||
fits_part_type_id: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
ap = await augmented_part_crud.get(db, augmented_part_id)
|
||||
if ap is None:
|
||||
raise HTTPException(status_code=404, detail="Scope not found")
|
||||
label = ap.sku or ap.description
|
||||
await augmented_part_crud.delete(db, ap)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="delete",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=augmented_part_id,
|
||||
description=audit_crud.describe_delete(
|
||||
"AugmentedPart", augmented_part_id, label
|
||||
),
|
||||
for augmented_part_id in scope_ids:
|
||||
ap = await augmented_part_crud.get(db, augmented_part_id)
|
||||
if ap is None:
|
||||
continue
|
||||
label = ap.sku or ap.description
|
||||
await augmented_part_crud.delete(db, ap)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="delete",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=augmented_part_id,
|
||||
description=audit_crud.describe_delete(
|
||||
"AugmentedPart", augmented_part_id, label
|
||||
),
|
||||
)
|
||||
filters = {
|
||||
"description": description,
|
||||
"manufacturer_id": manufacturer_id,
|
||||
"scope_model_id": scope_model_id,
|
||||
"sub_part_type_id": sub_part_type_id,
|
||||
"fits_part_type_id": fits_part_type_id,
|
||||
}
|
||||
guide = await _build(db, filters)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"_scopes_table_card.html",
|
||||
{**guide, "can_write_scopes": _can_write_scopes(actor)},
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
Reference in new issue
Block a user