Upgrade code
This commit is contained in:
@@ -8,6 +8,11 @@
|
||||
<div class="card-body">
|
||||
<form method="GET" class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" class="form-control" name="q" value="{{ search }}"
|
||||
placeholder="Title, ticket #, submitter, assignee, comments…"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
@@ -16,7 +21,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Priority</label>
|
||||
<select class="form-select" name="priority">
|
||||
<option value="">All Priorities</option>
|
||||
@@ -25,7 +30,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Assignment</label>
|
||||
<select class="form-select" name="assigned">
|
||||
<option value="">All</option>
|
||||
@@ -35,7 +40,11 @@
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-fill"><i class="bi bi-search me-1"></i>Filter</button>
|
||||
<a href="{{ url_for('admin.all_tickets') }}" class="btn btn-secondary"><i class="bi bi-x-lg"></i></a>
|
||||
<a href="{{ url_for('admin.all_tickets') }}" class="btn btn-secondary" title="Clear filters"><i class="bi bi-x-lg"></i></a>
|
||||
<a href="{{ url_for('admin.export_tickets', status=status, priority=priority, assigned=assigned, q=search) }}"
|
||||
class="btn btn-secondary" title="Export current results to CSV">
|
||||
<i class="bi bi-download me-1"></i>CSV
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -83,19 +92,19 @@
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav><ul class="pagination mb-0">
|
||||
{% if tickets.has_prev %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.prev_num, status=status, priority=priority, assigned=assigned) }}">‹</a></li>
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.prev_num, status=status, priority=priority, assigned=assigned, q=search) }}">‹</a></li>
|
||||
{% endif %}
|
||||
{% for p in tickets.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p==tickets.page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('admin.all_tickets', page=p, status=status, priority=priority, assigned=assigned) }}">{{ p }}</a>
|
||||
<a class="page-link" href="{{ url_for('admin.all_tickets', page=p, status=status, priority=priority, assigned=assigned, q=search) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if tickets.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.next_num, status=status, priority=priority, assigned=assigned) }}">›</a></li>
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.next_num, status=status, priority=priority, assigned=assigned, q=search) }}">›</a></li>
|
||||
{% endif %}
|
||||
</ul></nav>
|
||||
</div>
|
||||
|
||||
@@ -8,17 +8,30 @@
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-person-circle me-2"></i>Account Settings</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<!-- Avatar placeholder -->
|
||||
<!-- Avatar -->
|
||||
<div class="text-center mb-4">
|
||||
{% if current_user.avatar_url %}
|
||||
<img src="{{ url_for('auth.serve_avatar', filename=current_user.avatar_url) }}"
|
||||
alt="Avatar"
|
||||
style="width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--border);margin:0 auto 10px;display:block;"/>
|
||||
{% else %}
|
||||
<div style="width:80px;height:80px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#fff;margin:0 auto 10px;">
|
||||
{{ current_user.full_name[0].upper() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="font-size:12px;color:var(--muted);">{{ current_user.email }}</div>
|
||||
<div style="font-size:11px;background:rgba(0,180,216,.1);color:var(--accent3);display:inline-block;padding:2px 10px;border-radius:12px;margin-top:4px;">
|
||||
{{ current_user.role.replace('_',' ').upper() }}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="form-label" style="font-size:12px;">Profile Picture
|
||||
<span style="color:var(--muted);font-weight:400;">(PNG, JPG, GIF, WebP — max 5 MB)</span>
|
||||
</label>
|
||||
<input type="file" class="form-control form-control-sm" name="avatar"
|
||||
accept=".png,.jpg,.jpeg,.gif,.webp" style="max-width:300px;margin:0 auto;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
+19
-2
@@ -254,7 +254,16 @@
|
||||
.comment-card.internal{border-left:3px solid var(--warning);background:var(--warning-bg);}
|
||||
.comment-author{font-size:13px;font-weight:600;color:var(--text);}
|
||||
.comment-time{font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;}
|
||||
.comment-body{font-size:14px;margin-top:8px;white-space:pre-wrap;color:var(--text2);}
|
||||
.comment-body{font-size:14px;margin-top:8px;color:var(--text2);line-height:1.65;}
|
||||
.comment-body p{margin:0 0 8px;}
|
||||
.comment-body p:last-child{margin-bottom:0;}
|
||||
.comment-body ul,.comment-body ol{margin:0 0 8px;padding-left:20px;}
|
||||
.comment-body pre{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:10px 14px;font-size:12px;overflow-x:auto;margin:0 0 8px;}
|
||||
.comment-body code{background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-size:12px;font-family:'Space Mono',monospace;}
|
||||
.comment-body pre code{background:none;border:none;padding:0;}
|
||||
.comment-body blockquote{border-left:3px solid var(--border);margin:0 0 8px;padding:4px 12px;color:var(--muted);}
|
||||
.comment-body a{color:var(--accent3);}
|
||||
.comment-body hr{border:none;border-top:1px solid var(--border);margin:10px 0;}
|
||||
.history-item{font-size:12px;color:var(--muted);padding:6px 0;border-bottom:1px solid var(--border);}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@@ -332,7 +341,15 @@
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-card">
|
||||
<div class="avatar">{{ current_user.full_name[0].upper() }}</div>
|
||||
<div class="avatar">
|
||||
{% if current_user.avatar_url %}
|
||||
<img src="{{ url_for('auth.serve_avatar', filename=current_user.avatar_url) }}"
|
||||
alt="{{ current_user.full_name[0].upper() }}"
|
||||
style="width:32px;height:32px;border-radius:50%;object-fit:cover;display:block;"/>
|
||||
{% else %}
|
||||
{{ current_user.full_name[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="user-name">{{ current_user.full_name }}</div>
|
||||
<div class="user-role">{{ current_user.role.replace('_',' ') }}</div>
|
||||
|
||||
@@ -38,14 +38,25 @@
|
||||
{% if ticket_atts %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><i class="bi bi-paperclip me-2"></i>Attachments</div>
|
||||
<div class="card-body d-flex flex-wrap gap-2">
|
||||
{% for att in ticket_atts %}
|
||||
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-download me-1"></i>{{ att.filename }}
|
||||
<span style="font-size:10px;color:var(--muted);">({{ (att.file_size/1024)|int }}KB)</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-start">
|
||||
{% for att in ticket_atts %}
|
||||
{% if att.mime_type and att.mime_type.startswith('image/') %}
|
||||
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
|
||||
target="_blank" class="comment-img-link" title="{{ att.filename }}">
|
||||
<img src="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
|
||||
alt="{{ att.filename }}"
|
||||
class="comment-img-thumb"/>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-download me-1"></i>{{ att.filename }}
|
||||
<span style="font-size:10px;color:var(--muted);">({{ (att.file_size/1024)|int }}KB)</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -77,8 +88,14 @@
|
||||
<div class="comment-card {% if comment.is_internal %}internal{% endif %}" id="comment-{{ comment.id }}">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;overflow:hidden;flex-shrink:0;">
|
||||
{% if comment.author.avatar_url %}
|
||||
<img src="{{ url_for('auth.serve_avatar', filename=comment.author.avatar_url) }}"
|
||||
alt="{{ comment.author.full_name[0].upper() }}"
|
||||
style="width:28px;height:28px;object-fit:cover;display:block;"/>
|
||||
{% else %}
|
||||
{{ comment.author.full_name[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="comment-author">{{ comment.author.full_name }}</span>
|
||||
{% if comment.author.is_it_staff %}
|
||||
@@ -102,7 +119,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-body">{{ comment.body }}</div>
|
||||
<div class="comment-body">{{ comment.body | safe }}</div>
|
||||
{% set c_atts = comment.attachments.all() %}
|
||||
{% if c_atts %}
|
||||
<div class="mt-2">
|
||||
@@ -141,7 +158,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="mb-3">
|
||||
<textarea id="comment-body" class="form-control" name="body" rows="4" required
|
||||
placeholder="Add your update, follow-up, or response here… (you can also paste images directly)"></textarea>
|
||||
placeholder="Add your update… Markdown supported: **bold**, _italic_, `code`, - lists, > quotes (you can also paste images directly)"></textarea>
|
||||
</div>
|
||||
<!-- Image preview strip — populated by paste or file picker -->
|
||||
<div id="img-preview-strip" style="display:none;flex-wrap:wrap;gap:8px;margin-bottom:12px;"></div>
|
||||
@@ -200,11 +217,41 @@ if (typeof socket !== 'undefined') {
|
||||
// on submit, since a paste event cannot modify a real <input type=file>.
|
||||
let pastedFiles = [];
|
||||
|
||||
// ── Attachment limits (enforced client-side here AND server-side in tickets.py)
|
||||
const MAX_ATTACHMENT_FILES = 5;
|
||||
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25 MB total per comment
|
||||
|
||||
function getAttachmentError(allFiles) {
|
||||
if (allFiles.length > MAX_ATTACHMENT_FILES) {
|
||||
return `Too many files — maximum ${MAX_ATTACHMENT_FILES} attachments per comment.`;
|
||||
}
|
||||
const totalBytes = allFiles.reduce((sum, f) => sum + f.size, 0);
|
||||
if (totalBytes > MAX_ATTACHMENT_BYTES) {
|
||||
const mb = (MAX_ATTACHMENT_BYTES / (1024 * 1024)).toFixed(0);
|
||||
return `Total attachment size exceeds the ${mb} MB limit per comment.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rebuildPreviewStrip() {
|
||||
// Collect all files: pasted images + files chosen via the picker
|
||||
const pickerFiles = Array.from(document.getElementById('attachment-input').files || []);
|
||||
const allFiles = [...pastedFiles, ...pickerFiles];
|
||||
const strip = document.getElementById('img-preview-strip');
|
||||
const err = document.getElementById('comment-error');
|
||||
|
||||
// Show/clear the limit error alongside the preview strip
|
||||
const limitErr = getAttachmentError(allFiles);
|
||||
if (limitErr) {
|
||||
err.textContent = limitErr;
|
||||
err.style.display = 'block';
|
||||
} else {
|
||||
// Only clear the error if it was an attachment limit message
|
||||
if (err.textContent && err.textContent.includes('attachment')) {
|
||||
err.style.display = 'none';
|
||||
err.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiles.length === 0) { strip.style.display = 'none'; strip.innerHTML = ''; return; }
|
||||
|
||||
@@ -282,6 +329,18 @@ document.getElementById('comment-form').addEventListener('submit', async functio
|
||||
const fd = new FormData(this);
|
||||
pastedFiles.forEach(f => fd.append('attachments', f, f.name));
|
||||
|
||||
// Client-side attachment limit check — mirrors server-side guard in tickets.py
|
||||
const pickerFiles = Array.from(document.getElementById('attachment-input').files || []);
|
||||
const allFiles = [...pastedFiles, ...pickerFiles];
|
||||
const limitErr = getAttachmentError(allFiles);
|
||||
if (limitErr) {
|
||||
err.textContent = limitErr;
|
||||
err.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-send me-2"></i>Post Comment';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(window.location.pathname, {
|
||||
method : 'POST',
|
||||
@@ -408,11 +467,16 @@ function buildCommentEl(c) {
|
||||
</a>`;
|
||||
}).join('');
|
||||
|
||||
const avatarInner = c.author_avatar
|
||||
? `<img src="/auth/avatar/${c.author_avatar}" alt="${c.author_init}"
|
||||
style="width:28px;height:28px;object-fit:cover;display:block;border-radius:50%;"/>`
|
||||
: c.author_init;
|
||||
|
||||
wrap.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">
|
||||
${c.author_init}
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;overflow:hidden;flex-shrink:0;">
|
||||
${avatarInner}
|
||||
</div>
|
||||
<span class="comment-author">${c.author_name}</span>
|
||||
${itBadge}${intBadge}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<form method="GET" class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" class="form-control" name="q" value="{{ search }}" placeholder="Search by title, ticket #, description…"/>
|
||||
<input type="text" class="form-control" name="q" value="{{ search }}" placeholder="Search title, ticket #, description, comments, assignee…"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Status</label>
|
||||
|
||||
Reference in New Issue
Block a user