Compare commits
No commits in common. "7d8185384e64714ffecdbe63acba5157e64b96ce" and "23f6b4d3e7f8f6a6af9141799ad3bf76e619c1ea" have entirely different histories.
7d8185384e
...
23f6b4d3e7
@ -1 +1 @@
|
|||||||
v20260106-02-inbox-bulk-delete
|
v20260106-01-m365-combined-job-name-merge
|
||||||
|
|||||||
@ -69,8 +69,6 @@ def inbox():
|
|||||||
has_prev=has_prev,
|
has_prev=has_prev,
|
||||||
has_next=has_next,
|
has_next=has_next,
|
||||||
customers=customer_rows,
|
customers=customer_rows,
|
||||||
can_bulk_delete=(get_active_role() in ("admin", "operator")),
|
|
||||||
is_admin=(get_active_role() == "admin"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -298,62 +296,6 @@ def inbox_message_delete(message_id: int):
|
|||||||
return redirect(url_for("main.inbox"))
|
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")
|
@main_bp.route("/inbox/deleted")
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin")
|
@roles_required("admin")
|
||||||
|
|||||||
@ -56,26 +56,10 @@
|
|||||||
|
|
||||||
{{ pager("top", page, total_pages, has_prev, has_next) }}
|
{{ 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">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover align-middle" id="inboxTable">
|
<table class="table table-sm table-hover align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<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">From</th>
|
||||||
<th scope="col">Subject</th>
|
<th scope="col">Subject</th>
|
||||||
<th scope="col">Date / time</th>
|
<th scope="col">Date / time</th>
|
||||||
@ -91,11 +75,6 @@
|
|||||||
{% if rows %}
|
{% if rows %}
|
||||||
{% for row in rows %}
|
{% for row in rows %}
|
||||||
<tr class="inbox-row" data-message-id="{{ row.id }}" style="cursor: pointer;">
|
<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.from_address }}</td>
|
||||||
<td>{{ row.subject }}</td>
|
<td>{{ row.subject }}</td>
|
||||||
<td>{{ row.received_at }}</td>
|
<td>{{ row.received_at }}</td>
|
||||||
@ -211,125 +190,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var customers = {{ customers|tojson|safe }};
|
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) {
|
function wrapMailHtml(html) {
|
||||||
|
|||||||
@ -4,14 +4,6 @@
|
|||||||
- Job names containing the suffix "(Combined)" are now mapped to the same job as the corresponding job without this suffix.
|
- 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.
|
- 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
|
## v0.1.16
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user