05/22 Enhance codes and fix bugs
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
{% for days, colour, icon, label, hint in options %}
|
||||
<div class="col-6 col-lg-3">
|
||||
<form method="POST" action="{{ url_for('admin.activity_logs') }}"
|
||||
onsubmit="return confirmCleanup({{ days }}, {{ counts_by_retention[days] }})">
|
||||
onsubmit="return confirmCleanup(this, {{ days }}, {{ counts_by_retention[days] }})">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="days" value="{{ days }}"/>
|
||||
<button type="submit" class="btn btn-sm w-100 d-flex flex-column align-items-center py-3 gap-1"
|
||||
@@ -170,12 +170,17 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmCleanup(days, count) {
|
||||
function confirmCleanup(form, days, count) {
|
||||
if (count === 0) return false;
|
||||
return confirm(
|
||||
confirmModal(
|
||||
'Clean Up Activity Logs',
|
||||
'Delete ' + count.toLocaleString() + ' log entr' + (count === 1 ? 'y' : 'ies') +
|
||||
' older than ' + days + ' days?\n\nThis cannot be undone.'
|
||||
' older than ' + days + ' days? This action cannot be undone.',
|
||||
'Delete Logs',
|
||||
'btn-danger',
|
||||
() => { form._confirmed = true; form.submit(); }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -44,7 +44,7 @@
|
||||
class="btn btn-secondary btn-sm"><i class="bi bi-pencil"></i></a>
|
||||
<form method="POST"
|
||||
action="{{ url_for('admin.canned_response_delete', item_id=item.id) }}"
|
||||
onsubmit="return confirm('Delete this quick reply?')">
|
||||
data-confirm="Delete this quick reply?" data-confirm-title="Delete Quick Reply" data-confirm-ok="Delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
|
||||
@@ -369,12 +369,10 @@ function loadExistingAtts() {
|
||||
}
|
||||
|
||||
function deleteAtt(attId, deleteUrl, filename) {
|
||||
if (!confirm('Delete "' + filename + '"?')) return;
|
||||
|
||||
const row = document.getElementById('att-row-' + attId);
|
||||
if (row) { row.style.opacity = '0.4'; row.style.pointerEvents = 'none'; }
|
||||
|
||||
fetch(deleteUrl, { method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content } })
|
||||
confirmModal('Delete Attachment', 'Delete "' + filename + '"? This cannot be undone.', 'Delete', 'btn-danger', () => {
|
||||
const row = document.getElementById('att-row-' + attId);
|
||||
if (row) { row.style.opacity = '0.4'; row.style.pointerEvents = 'none'; }
|
||||
fetch(deleteUrl, { method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content } })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
@@ -395,6 +393,7 @@ function deleteAtt(attId, deleteUrl, filename) {
|
||||
if (row) { row.style.opacity = ''; row.style.pointerEvents = ''; }
|
||||
alert('Network error. Please try again.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load existing attachments on page load (only for edit mode)
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('admin.kb_delete', article_id=art.id) }}" onsubmit="return confirm('Delete this article?');" style="margin:0;">
|
||||
<form method="POST" action="{{ url_for('admin.kb_delete', article_id=art.id) }}" data-confirm="Delete this article? This cannot be undone." data-confirm-title="Delete Article" data-confirm-ok="Delete" style="margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm" style="background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);color:var(--danger);" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
|
||||
@@ -368,7 +368,8 @@
|
||||
{% if survey_enabled %}
|
||||
<button type="submit" name="survey_enabled" value="0"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Disable satisfaction surveys? No survey emails will be sent when tickets are resolved.')">
|
||||
data-confirm="Disable satisfaction surveys? No survey emails will be sent when tickets are resolved."
|
||||
data-confirm-title="Disable Satisfaction Surveys" data-confirm-ok="Disable">
|
||||
<i class="bi bi-toggle-on me-1"></i>Disable
|
||||
</button>
|
||||
{% else %}
|
||||
@@ -416,7 +417,8 @@
|
||||
{% if registration_enabled %}
|
||||
<button type="submit" name="registration_enabled" value="0"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Disable self-registration? New users will not be able to sign up independently.')">
|
||||
data-confirm="Disable self-registration? New users will not be able to sign up independently."
|
||||
data-confirm-title="Disable Self-Registration" data-confirm-ok="Disable">
|
||||
<i class="bi bi-toggle-on me-1"></i>Disable
|
||||
</button>
|
||||
{% else %}
|
||||
@@ -461,13 +463,14 @@ function syncColorPicker(val) {
|
||||
}
|
||||
|
||||
function removeLogo() {
|
||||
if (!confirm('Remove the current logo?')) return;
|
||||
document.getElementById('remove-logo-input').value = '1';
|
||||
const initials = document.getElementById('logo-initials').value.substring(0, 2).toUpperCase();
|
||||
document.getElementById('logo-preview-wrap').innerHTML =
|
||||
`<span id="logo-preview-initials" style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">${initials}</span>`;
|
||||
const row = document.getElementById('remove-logo-row');
|
||||
if (row) row.style.display = 'none';
|
||||
confirmModal('Remove Logo', 'Remove the current logo? The initials fallback will be displayed instead.', 'Remove', 'btn-danger', () => {
|
||||
document.getElementById('remove-logo-input').value = '1';
|
||||
const initials = document.getElementById('logo-initials').value.substring(0, 2).toUpperCase();
|
||||
document.getElementById('logo-preview-wrap').innerHTML =
|
||||
`<span id="logo-preview-initials" style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">${initials}</span>`;
|
||||
const row = document.getElementById('remove-logo-row');
|
||||
if (row) row.style.display = 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
<a href="{{ url_for('admin.ticket_template_edit', tmpl_id=t.id) }}"
|
||||
class="btn btn-secondary btn-sm" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||
<form method="POST" action="{{ url_for('admin.ticket_template_delete', tmpl_id=t.id) }}"
|
||||
onsubmit="return confirm('Delete template "{{ t.name }}"?');" style="margin:0;">
|
||||
data-confirm="Delete template {{ t.name }}? This cannot be undone."
|
||||
data-confirm-title="Delete Template" data-confirm-ok="Delete" style="margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm"
|
||||
style="background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);color:var(--danger);"
|
||||
|
||||
@@ -60,7 +60,8 @@
|
||||
</a>
|
||||
{% if u.id != current_user.id and u.is_active %}
|
||||
<form method="POST" action="{{ url_for('admin.delete_user', user_id=u.id) }}"
|
||||
onsubmit="return confirm('Deactivate {{ u.full_name }}?');">
|
||||
data-confirm="Deactivate {{ u.full_name }}? They will no longer be able to log in."
|
||||
data-confirm-title="Deactivate User" data-confirm-ok="Deactivate" data-confirm-class="btn-warning">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <button type="submit" class="btn btn-sm" style="background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);color:var(--danger);">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
|
||||
+74
-19
@@ -8,21 +8,8 @@
|
||||
<title id="page-title-text">{% block title %}IT Helpdesk{% endblock %} — {{ branding.app_name }}</title>
|
||||
<style>
|
||||
:root {
|
||||
--accent: {
|
||||
{
|
||||
branding.primary_color
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
--accent-h: {
|
||||
{
|
||||
branding.primary_color
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
--accent: {{ branding.primary_color }};
|
||||
--accent-h: {{ branding.primary_color }};
|
||||
}
|
||||
</style>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
@@ -1270,6 +1257,25 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Reusable confirmation modal ─────────────────────────────────────────── -->
|
||||
<div class="modal fade" id="confirm-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="padding:14px 20px;">
|
||||
<h5 class="modal-title" id="confirm-modal-title" style="font-size:15px;font-weight:600;"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding:16px 20px;">
|
||||
<p id="confirm-modal-body" style="font-size:14px;color:var(--text2);margin:0;"></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding:12px 20px;gap:8px;">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" id="confirm-modal-ok" class="btn btn-sm btn-danger">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<script>
|
||||
@@ -1278,6 +1284,55 @@
|
||||
// Use csrfPost(url, body) instead of raw fetch(..., {method:'POST'}) to ensure
|
||||
// the token from the <meta> tag is sent automatically.
|
||||
const _csrfToken = () => document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
// HTML-escape helper — use whenever inserting server-supplied strings into innerHTML.
|
||||
function _esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = String(s == null ? '' : s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ── Confirmation modal ────────────────────────────────────────────────────────
|
||||
// Use instead of window.confirm() for all destructive actions.
|
||||
// Forms: add data-confirm="message" (and optionally data-confirm-title, data-confirm-ok,
|
||||
// data-confirm-class) — the interceptor below handles submission automatically.
|
||||
// JS callers: call confirmModal(title, message, okText, okClass, callback) directly.
|
||||
function confirmModal(title, message, okText, okClass, onConfirm) {
|
||||
const modal = document.getElementById('confirm-modal');
|
||||
if (!modal) { if (window.confirm(message)) onConfirm(); return; }
|
||||
document.getElementById('confirm-modal-title').textContent = title || 'Confirm Action';
|
||||
document.getElementById('confirm-modal-body').textContent = message || 'Are you sure?';
|
||||
const okBtn = document.getElementById('confirm-modal-ok');
|
||||
okBtn.textContent = okText || 'Confirm';
|
||||
okBtn.className = 'btn btn-sm ' + (okClass || 'btn-danger');
|
||||
// Clone to drop any previous click listener
|
||||
const fresh = okBtn.cloneNode(true);
|
||||
okBtn.replaceWith(fresh);
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
fresh.addEventListener('click', () => { bsModal.hide(); onConfirm(); });
|
||||
bsModal.show();
|
||||
}
|
||||
|
||||
// Auto-intercept any <form data-confirm="..."> or submit button with data-confirm.
|
||||
document.addEventListener('submit', function(e) {
|
||||
const form = e.target;
|
||||
const submitter = e.submitter; // the button that triggered submit
|
||||
const source = submitter || form;
|
||||
const msg = source.dataset.confirm || form.dataset.confirm;
|
||||
if (!msg || form._confirmed) { if (form._confirmed) form._confirmed = false; return; }
|
||||
e.preventDefault();
|
||||
confirmModal(
|
||||
source.dataset.confirmTitle || form.dataset.confirmTitle || 'Confirm Action',
|
||||
msg,
|
||||
source.dataset.confirmOk || form.dataset.confirmOk || 'Confirm',
|
||||
source.dataset.confirmClass || form.dataset.confirmClass || 'btn-danger',
|
||||
() => {
|
||||
form._confirmed = true;
|
||||
form.requestSubmit ? form.requestSubmit(submitter || null) : form.submit();
|
||||
}
|
||||
);
|
||||
}, true);
|
||||
|
||||
async function csrfPost(url, body = null) {
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
@@ -1373,9 +1428,9 @@
|
||||
`style="cursor:${n.link ? 'pointer' : 'default'}">` +
|
||||
dot +
|
||||
`<div style="flex:1;min-width:0;">` +
|
||||
`<div class="notif-title">${n.title}</div>` +
|
||||
`<div class="notif-msg">${n.message || ''}</div>` +
|
||||
`<div class="notif-time">${n.created_at}</div>` +
|
||||
`<div class="notif-title">${_esc(n.title)}</div>` +
|
||||
`<div class="notif-msg">${_esc(n.message)}</div>` +
|
||||
`<div class="notif-time">${_esc(n.created_at)}</div>` +
|
||||
`</div></div>`
|
||||
);
|
||||
}
|
||||
@@ -1459,7 +1514,7 @@
|
||||
function showToast(title, msg) {
|
||||
const t = document.createElement('div');
|
||||
t.style.cssText = 'position:fixed;bottom:90px;right:28px;background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent3);border-radius:8px;padding:12px 16px;z-index:300;max-width:300px;box-shadow:0 4px 16px rgba(0,0,0,.4);animation:slideIn .25s ease;';
|
||||
t.innerHTML = `<div style="font-size:13px;font-weight:600;margin-bottom:4px;">${title}</div><div style="font-size:12px;color:var(--muted);">${msg || ''}</div>`;
|
||||
t.innerHTML = `<div style="font-size:13px;font-weight:600;margin-bottom:4px;">${_esc(title)}</div><div style="font-size:12px;color:var(--muted);">${_esc(msg)}</div>`;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 5000);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,8 @@
|
||||
<span class="comment-time">{{ comment.created_at | localtime("%b %d, %Y %H:%M") }}</span>
|
||||
{% if current_user.is_it_staff or comment.author_id == current_user.id %}
|
||||
<form method="POST" action="{{ url_for('tickets.delete_comment', comment_id=comment.id) }}"
|
||||
onsubmit="return confirm('Delete this comment?');">
|
||||
data-confirm="Delete this comment? This cannot be undone."
|
||||
data-confirm-title="Delete Comment" data-confirm-ok="Delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm" style="background:none;border:none;color:var(--muted);padding:2px 6px;"
|
||||
title="Delete comment">
|
||||
@@ -502,7 +503,7 @@ function buildCommentEl(c) {
|
||||
? '<span style="font-size:10px;background:rgba(251,191,36,.15);color:var(--warning);padding:1px 7px;border-radius:4px;font-weight:600;">INTERNAL NOTE</span>'
|
||||
: '';
|
||||
const deleteBtn = c.can_delete
|
||||
? `<form method="POST" action="${DELETE_COMMENT_URL.replace('{id}', c.id)}" onsubmit="return confirm('Delete this comment?');" style="margin:0;">
|
||||
? `<form method="POST" action="${DELETE_COMMENT_URL.replace('{id}', c.id)}" data-confirm="Delete this comment? This cannot be undone." data-confirm-title="Delete Comment" data-confirm-ok="Delete" style="margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="${document.querySelector('meta[name=csrf-token]').content}"/>
|
||||
<button type="submit" class="btn btn-sm" style="background:none;border:none;color:var(--muted);padding:2px 6px;" title="Delete comment">
|
||||
<i class="bi bi-trash"></i>
|
||||
@@ -654,7 +655,8 @@ function buildCommentEl(c) {
|
||||
</div>
|
||||
<form method="POST"
|
||||
action="{{ url_for('tickets.unlink_ticket', ticket_id=ticket.id, link_id=lnk.id) }}"
|
||||
onsubmit="return confirm('Remove this link?')" style="margin:0;flex-shrink:0;">
|
||||
data-confirm="Remove this link?" data-confirm-title="Remove Link" data-confirm-ok="Remove"
|
||||
style="margin:0;flex-shrink:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm" title="Remove link"
|
||||
style="background:none;border:none;color:var(--muted);padding:2px 6px;">
|
||||
|
||||
Reference in New Issue
Block a user