feat: issues board - add submitter name, comments, and edit support
This commit is contained in:
parent
21e72b438f
commit
f96171a345
3 changed files with 313 additions and 28 deletions
192
static/script.js
192
static/script.js
|
|
@ -4135,6 +4135,12 @@ const ISSUE_CATEGORIES = {
|
|||
other: { label: 'Other', color: '#888888' },
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showIssuesWindow() {
|
||||
const { win, content, isNew } = createWindow(
|
||||
'issuesWindow', 'Issues Board', 'issues-window'
|
||||
|
|
@ -4145,8 +4151,8 @@ function showIssuesWindow() {
|
|||
return;
|
||||
}
|
||||
|
||||
win.style.width = '500px';
|
||||
win.style.height = '450px';
|
||||
win.style.width = '540px';
|
||||
win.style.height = '520px';
|
||||
|
||||
// Issues list container
|
||||
const listDiv = document.createElement('div');
|
||||
|
|
@ -4159,6 +4165,7 @@ function showIssuesWindow() {
|
|||
form.className = 'issues-form';
|
||||
form.innerHTML = `
|
||||
<div style="display:flex;gap:4px;margin-bottom:4px;">
|
||||
<input type="text" id="issueAuthor" placeholder="Your name..." style="width:120px;padding:3px 6px;font-size:0.8rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||
<input type="text" id="issueTitle" placeholder="Issue title..." style="flex:1;padding:3px 6px;font-size:0.8rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||
<select id="issueCategory" style="padding:3px;font-size:0.75rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||
<option value="plugin">Plugin</option>
|
||||
|
|
@ -4175,17 +4182,24 @@ function showIssuesWindow() {
|
|||
`;
|
||||
content.appendChild(form);
|
||||
|
||||
// Remember author name in localStorage
|
||||
const authorInput = form.querySelector('#issueAuthor');
|
||||
authorInput.value = localStorage.getItem('issueAuthorName') || '';
|
||||
|
||||
// Add button handler
|
||||
form.querySelector('#issueAddBtn').addEventListener('click', async () => {
|
||||
const author = document.getElementById('issueAuthor').value.trim() || 'Anonymous';
|
||||
const title = document.getElementById('issueTitle').value.trim();
|
||||
const desc = document.getElementById('issueDescription').value.trim();
|
||||
const cat = document.getElementById('issueCategory').value;
|
||||
if (!title) return;
|
||||
|
||||
localStorage.setItem('issueAuthorName', author);
|
||||
|
||||
await fetch('/issues', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, description: desc, category: cat, author: 'User' })
|
||||
body: JSON.stringify({ title, description: desc, category: cat, author })
|
||||
});
|
||||
document.getElementById('issueTitle').value = '';
|
||||
document.getElementById('issueDescription').value = '';
|
||||
|
|
@ -4217,24 +4231,38 @@ async function refreshIssuesList(win) {
|
|||
const cat = ISSUE_CATEGORIES[issue.category] || ISSUE_CATEGORIES.other;
|
||||
const date = issue.created ? new Date(issue.created).toLocaleDateString('sv-SE') : '';
|
||||
const isResolved = !!issue.resolved;
|
||||
const author = issue.author || 'User';
|
||||
const comments = issue.comments || [];
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'issue-row' + (isResolved ? ' issue-resolved' : '');
|
||||
row.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span class="issue-category" style="background:${cat.color}">${cat.label}</span>
|
||||
<strong style="font-size:0.8rem;">${issue.title}</strong>
|
||||
<span style="margin-left:auto;color:#666;font-size:0.65rem;">${date}</span>
|
||||
</div>
|
||||
${issue.description ? `<div class="issue-description">${issue.description}</div>` : ''}
|
||||
`;
|
||||
|
||||
const headerDiv = row.querySelector('div');
|
||||
// Header line
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.className = 'issue-header';
|
||||
headerDiv.innerHTML = `
|
||||
<span class="issue-category" style="background:${cat.color}">${cat.label}</span>
|
||||
<strong style="font-size:0.8rem;">${escapeHtml(issue.title)}</strong>
|
||||
<span class="issue-author">by ${escapeHtml(author)}</span>
|
||||
<span style="margin-left:auto;color:#666;font-size:0.65rem;">${date}</span>
|
||||
`;
|
||||
row.appendChild(headerDiv);
|
||||
|
||||
// Description
|
||||
if (issue.description) {
|
||||
const descDiv = document.createElement('div');
|
||||
descDiv.className = 'issue-description';
|
||||
descDiv.textContent = issue.description;
|
||||
row.appendChild(descDiv);
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'issue-actions';
|
||||
|
||||
if (isResolved) {
|
||||
// Reopen button
|
||||
const reopenBtn = document.createElement('button');
|
||||
reopenBtn.textContent = '↺ Reopen';
|
||||
reopenBtn.textContent = '\u21BA Reopen';
|
||||
reopenBtn.className = 'issue-reopen-btn';
|
||||
reopenBtn.addEventListener('click', async () => {
|
||||
await fetch(`/issues/${issue.id}`, {
|
||||
|
|
@ -4244,22 +4272,20 @@ async function refreshIssuesList(win) {
|
|||
});
|
||||
refreshIssuesList(win);
|
||||
});
|
||||
headerDiv.appendChild(reopenBtn);
|
||||
actionsDiv.appendChild(reopenBtn);
|
||||
|
||||
// Delete button
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.textContent = '🗑 Delete';
|
||||
deleteBtn.textContent = '\uD83D\uDDD1 Delete';
|
||||
deleteBtn.className = 'issue-delete-btn';
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!confirm(`Delete issue "${issue.title}"?`)) return;
|
||||
await fetch(`/issues/${issue.id}`, { method: 'DELETE' });
|
||||
refreshIssuesList(win);
|
||||
});
|
||||
headerDiv.appendChild(deleteBtn);
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
} else {
|
||||
// Resolve button (marks as resolved, doesn't delete)
|
||||
const resolveBtn = document.createElement('button');
|
||||
resolveBtn.textContent = '✓ Resolve';
|
||||
resolveBtn.textContent = '\u2713 Resolve';
|
||||
resolveBtn.className = 'issue-resolve-btn';
|
||||
resolveBtn.addEventListener('click', async () => {
|
||||
await fetch(`/issues/${issue.id}`, {
|
||||
|
|
@ -4269,12 +4295,136 @@ async function refreshIssuesList(win) {
|
|||
});
|
||||
refreshIssuesList(win);
|
||||
});
|
||||
headerDiv.appendChild(resolveBtn);
|
||||
actionsDiv.appendChild(resolveBtn);
|
||||
}
|
||||
|
||||
// Edit button
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.textContent = '\u270E Edit';
|
||||
editBtn.className = 'issue-edit-btn';
|
||||
editBtn.addEventListener('click', () => {
|
||||
showEditIssueForm(row, issue, win);
|
||||
});
|
||||
actionsDiv.appendChild(editBtn);
|
||||
|
||||
// Comment toggle button
|
||||
const commentBtn = document.createElement('button');
|
||||
commentBtn.textContent = `\uD83D\uDCAC ${comments.length}`;
|
||||
commentBtn.className = 'issue-comment-btn';
|
||||
commentBtn.addEventListener('click', () => {
|
||||
const existing = row.querySelector('.issue-comments-section');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
} else {
|
||||
showCommentsSection(row, issue, win);
|
||||
}
|
||||
});
|
||||
actionsDiv.appendChild(commentBtn);
|
||||
|
||||
row.appendChild(actionsDiv);
|
||||
listDiv.appendChild(row);
|
||||
});
|
||||
} catch (err) {
|
||||
listDiv.innerHTML = '<div style="padding:10px;color:#c44;font-size:0.8rem;">Failed to load issues</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function showEditIssueForm(row, issue, win) {
|
||||
// Remove any existing edit form
|
||||
const existing = row.querySelector('.issue-edit-form');
|
||||
if (existing) { existing.remove(); return; }
|
||||
|
||||
const cat = issue.category || 'other';
|
||||
const form = document.createElement('div');
|
||||
form.className = 'issue-edit-form';
|
||||
form.innerHTML = `
|
||||
<div style="display:flex;gap:4px;margin-bottom:4px;">
|
||||
<input type="text" class="edit-title" value="${escapeHtml(issue.title)}" style="flex:1;padding:3px 6px;font-size:0.8rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||
<select class="edit-category" style="padding:3px;font-size:0.75rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||
<option value="plugin"${cat === 'plugin' ? ' selected' : ''}>Plugin</option>
|
||||
<option value="overlord"${cat === 'overlord' ? ' selected' : ''}>Overlord</option>
|
||||
<option value="nav"${cat === 'nav' ? ' selected' : ''}>Nav</option>
|
||||
<option value="macro"${cat === 'macro' ? ' selected' : ''}>Macro</option>
|
||||
<option value="other"${cat === 'other' ? ' selected' : ''}>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<textarea class="edit-description" rows="2" style="flex:1;padding:3px 6px;font-size:0.75rem;border:1px solid #555;background:#2a2a2a;color:#ddd;resize:vertical;">${escapeHtml(issue.description || '')}</textarea>
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<button class="edit-save-btn" style="padding:3px 8px;background:#4a80c0;color:#fff;border:1px solid #336699;cursor:pointer;font-size:0.7rem;">Save</button>
|
||||
<button class="edit-cancel-btn" style="padding:3px 8px;background:#444;color:#ccc;border:1px solid #555;cursor:pointer;font-size:0.7rem;">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
form.querySelector('.edit-save-btn').addEventListener('click', async () => {
|
||||
const title = form.querySelector('.edit-title').value.trim();
|
||||
const desc = form.querySelector('.edit-description').value.trim();
|
||||
const category = form.querySelector('.edit-category').value;
|
||||
if (!title) return;
|
||||
await fetch(`/issues/${issue.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, description: desc, category })
|
||||
});
|
||||
refreshIssuesList(win);
|
||||
});
|
||||
|
||||
form.querySelector('.edit-cancel-btn').addEventListener('click', () => {
|
||||
form.remove();
|
||||
});
|
||||
|
||||
row.appendChild(form);
|
||||
}
|
||||
|
||||
function showCommentsSection(row, issue, win) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'issue-comments-section';
|
||||
|
||||
const comments = issue.comments || [];
|
||||
|
||||
// Render existing comments
|
||||
const commentsListDiv = document.createElement('div');
|
||||
commentsListDiv.className = 'issue-comments-list';
|
||||
if (comments.length === 0) {
|
||||
commentsListDiv.innerHTML = '<div style="color:#666;font-size:0.7rem;padding:2px 0;">No comments yet</div>';
|
||||
} else {
|
||||
comments.forEach(c => {
|
||||
const cDiv = document.createElement('div');
|
||||
cDiv.className = 'issue-comment';
|
||||
const cDate = c.created ? new Date(c.created).toLocaleDateString('sv-SE') : '';
|
||||
cDiv.innerHTML = `<span class="comment-author">${escapeHtml(c.author || 'Anonymous')}</span> <span class="comment-date">${cDate}</span>`;
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'comment-text';
|
||||
textDiv.textContent = c.text;
|
||||
cDiv.appendChild(textDiv);
|
||||
commentsListDiv.appendChild(cDiv);
|
||||
});
|
||||
}
|
||||
section.appendChild(commentsListDiv);
|
||||
|
||||
// Add comment form
|
||||
const addDiv = document.createElement('div');
|
||||
addDiv.className = 'issue-comment-form';
|
||||
addDiv.innerHTML = `
|
||||
<div style="display:flex;gap:4px;">
|
||||
<input type="text" class="comment-text-input" placeholder="Add a comment..." style="flex:1;padding:3px 6px;font-size:0.75rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||
<button class="comment-add-btn" style="padding:3px 8px;background:#4a80c0;color:#fff;border:1px solid #336699;cursor:pointer;font-size:0.7rem;">Post</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
addDiv.querySelector('.comment-add-btn').addEventListener('click', async () => {
|
||||
const text = addDiv.querySelector('.comment-text-input').value.trim();
|
||||
if (!text) return;
|
||||
const author = localStorage.getItem('issueAuthorName') || 'Anonymous';
|
||||
await fetch(`/issues/${issue.id}/comments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, author })
|
||||
});
|
||||
refreshIssuesList(win);
|
||||
});
|
||||
|
||||
section.appendChild(addDiv);
|
||||
row.appendChild(section);
|
||||
}
|
||||
|
|
|
|||
102
static/style.css
102
static/style.css
|
|
@ -2644,8 +2644,8 @@ table.ts-allegiance td:first-child {
|
|||
/* ─── Issues Board ─── */
|
||||
|
||||
.issues-window {
|
||||
width: 500px;
|
||||
height: 450px;
|
||||
width: 540px;
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
.issues-window .window-content {
|
||||
|
|
@ -2672,6 +2672,18 @@ table.ts-allegiance td:first-child {
|
|||
background: #1a1a2a;
|
||||
}
|
||||
|
||||
.issue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.issue-author {
|
||||
color: #8a8a6a;
|
||||
font-size: 0.65rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.issue-row .issue-description {
|
||||
color: #aaa;
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -2703,15 +2715,22 @@ table.ts-allegiance td:first-child {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.issue-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.issue-resolve-btn,
|
||||
.issue-reopen-btn,
|
||||
.issue-delete-btn {
|
||||
.issue-delete-btn,
|
||||
.issue-edit-btn,
|
||||
.issue-comment-btn {
|
||||
padding: 1px 6px;
|
||||
font-size: 0.65rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.issue-resolve-btn {
|
||||
|
|
@ -2744,8 +2763,83 @@ table.ts-allegiance td:first-child {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.issue-edit-btn {
|
||||
color: #ca4;
|
||||
border: 1px solid #ca4;
|
||||
}
|
||||
|
||||
.issue-edit-btn:hover {
|
||||
background: #ca4;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.issue-comment-btn {
|
||||
color: #6af;
|
||||
border: 1px solid #6af;
|
||||
}
|
||||
|
||||
.issue-comment-btn:hover {
|
||||
background: #6af;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.issues-form {
|
||||
padding: 6px 8px;
|
||||
border-top: 1px solid #444;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Edit form */
|
||||
.issue-edit-form {
|
||||
margin-top: 6px;
|
||||
padding: 6px;
|
||||
background: #1a1a2a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Comments section */
|
||||
.issue-comments-section {
|
||||
margin-top: 6px;
|
||||
padding: 6px;
|
||||
background: #151520;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.issue-comments-list {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.issue-comment {
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.issue-comment:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
color: #8a8a6a;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
color: #555;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
color: #bbb;
|
||||
font-size: 0.75rem;
|
||||
padding-left: 4px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.issue-comment-form {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue