Auto-commit local changes before build (2026-01-06 10:02:17)
This commit is contained in:
parent
f14e02992d
commit
19f4b59e23
@ -1 +1 @@
|
||||
v20260106-01-m365-combined-job-name-merge
|
||||
v20260106-02-inbox-bulk-delete
|
||||
|
||||
@ -69,6 +69,8 @@ def inbox():
|
||||
has_prev=has_prev,
|
||||
has_next=has_next,
|
||||
customers=customer_rows,
|
||||
can_bulk_delete=(get_active_role() in ("admin", "operator")),
|
||||
is_admin=(get_active_role() == "admin"),
|
||||
)
|
||||
|
||||
|
||||
@ -296,6 +298,62 @@ def inbox_message_delete(message_id: int):
|
||||
return redirect(url_for("main.inbox"))
|
||||
|
||||
|
||||
|
||||
|
||||
@main_bp.post("/api/inbox/delete")
|
||||
@login_required
|
||||
@roles_required("admin", "operator")
|
||||
def api_inbox_bulk_delete():
|
||||
"""Bulk delete inbox messages (soft delete -> move to Deleted)."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
message_ids = data.get("message_ids") or []
|
||||
|
||||
try:
|
||||
message_ids = [int(x) for x in message_ids]
|
||||
except Exception:
|
||||
return jsonify({"status": "error", "message": "Invalid message_ids."}), 400
|
||||
|
||||
if not message_ids:
|
||||
return jsonify({"status": "ok", "updated": 0, "skipped": 0, "missing": 0})
|
||||
|
||||
msgs = MailMessage.query.filter(MailMessage.id.in_(message_ids)).all()
|
||||
msg_map = {int(m.id): m for m in msgs}
|
||||
|
||||
now = datetime.utcnow()
|
||||
updated = 0
|
||||
skipped = 0
|
||||
missing = 0
|
||||
|
||||
for mid in message_ids:
|
||||
msg = msg_map.get(int(mid))
|
||||
if not msg:
|
||||
missing += 1
|
||||
continue
|
||||
|
||||
if getattr(msg, "location", "inbox") != "inbox":
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if hasattr(msg, "location"):
|
||||
msg.location = "deleted"
|
||||
if hasattr(msg, "deleted_at"):
|
||||
msg.deleted_at = now
|
||||
if hasattr(msg, "deleted_by_user_id"):
|
||||
msg.deleted_by_user_id = current_user.id
|
||||
|
||||
updated += 1
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
_log_admin_event("inbox_bulk_delete_error", f"Failed to bulk delete inbox messages {message_ids}: {exc}")
|
||||
return jsonify({"status": "error", "message": "Database error while deleting messages."}), 500
|
||||
|
||||
_log_admin_event("inbox_bulk_delete", f"Deleted inbox messages: {message_ids}")
|
||||
return jsonify({"status": "ok", "updated": updated, "skipped": skipped, "missing": missing})
|
||||
|
||||
|
||||
@main_bp.route("/inbox/deleted")
|
||||
@login_required
|
||||
@roles_required("admin")
|
||||
|
||||
@ -56,10 +56,26 @@
|
||||
|
||||
{{ pager("top", page, total_pages, has_prev, has_next) }}
|
||||
|
||||
{% if can_bulk_delete %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" id="btn_inbox_delete_selected" disabled>Delete selected</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mb-2" id="inbox_status"></div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<table class="table table-sm table-hover align-middle" id="inboxTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
{% if can_bulk_delete %}
|
||||
<th scope="col" style="width: 34px;">
|
||||
<input class="form-check-input" type="checkbox" id="inbox_select_all" />
|
||||
</th>
|
||||
{% endif %}
|
||||
<th scope="col">From</th>
|
||||
<th scope="col">Subject</th>
|
||||
<th scope="col">Date / time</th>
|
||||
@ -75,6 +91,11 @@
|
||||
{% if rows %}
|
||||
{% for row in rows %}
|
||||
<tr class="inbox-row" data-message-id="{{ row.id }}" style="cursor: pointer;">
|
||||
{% if can_bulk_delete %}
|
||||
<td onclick="event.stopPropagation();">
|
||||
<input class="form-check-input inbox_row_cb" type="checkbox" value="{{ row.id }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ row.from_address }}</td>
|
||||
<td>{{ row.subject }}</td>
|
||||
<td>{{ row.received_at }}</td>
|
||||
@ -190,6 +211,125 @@
|
||||
(function () {
|
||||
var customers = {{ customers|tojson|safe }};
|
||||
|
||||
var table = document.getElementById('inboxTable');
|
||||
var selectAll = document.getElementById('inbox_select_all');
|
||||
var btnDeleteSelected = document.getElementById('btn_inbox_delete_selected');
|
||||
var statusEl = document.getElementById('inbox_status');
|
||||
|
||||
function getSelectedMessageIds() {
|
||||
if (!table) return [];
|
||||
var cbs = table.querySelectorAll('tbody .inbox_row_cb');
|
||||
var ids = [];
|
||||
cbs.forEach(function (cb) {
|
||||
if (cb.checked) ids.push(parseInt(cb.value, 10));
|
||||
});
|
||||
return ids.filter(function (x) { return Number.isFinite(x); });
|
||||
}
|
||||
|
||||
function refreshRowHighlights() {
|
||||
if (!table) return;
|
||||
var cbs = table.querySelectorAll('tbody .inbox_row_cb');
|
||||
cbs.forEach(function (cb) {
|
||||
var tr = cb.closest ? cb.closest('tr') : null;
|
||||
if (!tr) return;
|
||||
if (cb.checked) tr.classList.add('table-active');
|
||||
else tr.classList.remove('table-active');
|
||||
});
|
||||
}
|
||||
|
||||
function refreshSelectAll() {
|
||||
if (!selectAll || !table) return;
|
||||
var cbs = table.querySelectorAll('tbody .inbox_row_cb');
|
||||
var total = cbs.length;
|
||||
var checked = 0;
|
||||
cbs.forEach(function (cb) { if (cb.checked) checked++; });
|
||||
selectAll.indeterminate = checked > 0 && checked < total;
|
||||
selectAll.checked = total > 0 && checked === total;
|
||||
}
|
||||
|
||||
function updateBulkDeleteUi() {
|
||||
var ids = getSelectedMessageIds();
|
||||
refreshRowHighlights();
|
||||
if (btnDeleteSelected) btnDeleteSelected.disabled = ids.length === 0;
|
||||
if (statusEl) statusEl.textContent = ids.length ? (ids.length + ' selected') : '';
|
||||
refreshSelectAll();
|
||||
}
|
||||
|
||||
function postJson(url, payload) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(payload || {})
|
||||
}).then(function (r) {
|
||||
return r.json().then(function (data) { return { ok: r.ok, status: r.status, data: data }; });
|
||||
});
|
||||
}
|
||||
|
||||
if (selectAll && table) {
|
||||
function setAllSelection(checked) {
|
||||
var cbs = table.querySelectorAll('tbody .inbox_row_cb');
|
||||
cbs.forEach(function (cb) { cb.checked = !!checked; });
|
||||
selectAll.indeterminate = false;
|
||||
selectAll.checked = !!checked;
|
||||
setTimeout(function () {
|
||||
selectAll.indeterminate = false;
|
||||
selectAll.checked = !!checked;
|
||||
}, 0);
|
||||
updateBulkDeleteUi();
|
||||
}
|
||||
|
||||
selectAll.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
selectAll.addEventListener('change', function () {
|
||||
setAllSelection(selectAll.checked);
|
||||
});
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.addEventListener('change', function (e) {
|
||||
var t = e.target;
|
||||
if (t && t.classList && t.classList.contains('inbox_row_cb')) {
|
||||
updateBulkDeleteUi();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnDeleteSelected) {
|
||||
btnDeleteSelected.addEventListener('click', function () {
|
||||
var ids = getSelectedMessageIds();
|
||||
if (!ids.length) return;
|
||||
|
||||
var msg = 'Delete ' + ids.length + ' selected message' + (ids.length === 1 ? '' : 's') + ' from the Inbox?';
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
if (statusEl) statusEl.textContent = 'Deleting...';
|
||||
|
||||
postJson('{{ url_for('main.api_inbox_bulk_delete') }}', { message_ids: ids })
|
||||
.then(function (res) {
|
||||
if (!res.ok || !res.data || res.data.status !== 'ok') {
|
||||
var err = (res.data && (res.data.message || res.data.error)) ? (res.data.message || res.data.error) : 'Request failed.';
|
||||
if (statusEl) statusEl.textContent = err;
|
||||
alert(err);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(function () {
|
||||
var err = 'Request failed.';
|
||||
if (statusEl) statusEl.textContent = err;
|
||||
alert(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize UI state
|
||||
updateBulkDeleteUi();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function wrapMailHtml(html) {
|
||||
|
||||
@ -4,6 +4,14 @@
|
||||
- Job names containing the suffix "(Combined)" are now mapped to the same job as the corresponding job without this suffix.
|
||||
- This ensures that combined and non-combined backup result emails are aggregated under a single job in reports and statistics.
|
||||
|
||||
---
|
||||
|
||||
## v20260106-02-inbox-bulk-delete
|
||||
|
||||
- Added multi-select checkboxes to the Inbox for users with the operator or admin role.
|
||||
- Added a “Delete selected” bulk action that soft-deletes selected inbox messages (moves them to Deleted), including select-all support and a selected-count indicator.
|
||||
- Implemented a new POST API endpoint to bulk delete inbox messages with proper validation, skip/missing counts, and admin event logging.
|
||||
|
||||
|
||||
================================================================================================================================================
|
||||
## v0.1.16
|
||||
|
||||
Loading…
Reference in New Issue
Block a user