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
47
main.py
47
main.py
|
|
@ -1369,6 +1369,7 @@ async def add_issue(issue: dict):
|
||||||
"author": issue.get("author", "Anonymous").strip(),
|
"author": issue.get("author", "Anonymous").strip(),
|
||||||
"created": datetime.utcnow().isoformat(),
|
"created": datetime.utcnow().isoformat(),
|
||||||
"resolved": False,
|
"resolved": False,
|
||||||
|
"comments": [],
|
||||||
}
|
}
|
||||||
if not new_issue["title"]:
|
if not new_issue["title"]:
|
||||||
raise HTTPException(status_code=400, detail="Title is required")
|
raise HTTPException(status_code=400, detail="Title is required")
|
||||||
|
|
@ -1379,13 +1380,22 @@ async def add_issue(issue: dict):
|
||||||
|
|
||||||
@app.patch("/issues/{issue_id}")
|
@app.patch("/issues/{issue_id}")
|
||||||
async def update_issue(issue_id: str, update: dict):
|
async def update_issue(issue_id: str, update: dict):
|
||||||
"""Update an issue (e.g. toggle resolved state)."""
|
"""Update an issue (toggle resolved, edit title/description/category)."""
|
||||||
issues = _load_issues()
|
issues = _load_issues()
|
||||||
found = None
|
found = None
|
||||||
for i in issues:
|
for i in issues:
|
||||||
if i.get("id") == issue_id:
|
if i.get("id") == issue_id:
|
||||||
if "resolved" in update:
|
if "resolved" in update:
|
||||||
i["resolved"] = bool(update["resolved"])
|
i["resolved"] = bool(update["resolved"])
|
||||||
|
if "title" in update:
|
||||||
|
title = update["title"].strip()
|
||||||
|
if not title:
|
||||||
|
raise HTTPException(status_code=400, detail="Title cannot be empty")
|
||||||
|
i["title"] = title
|
||||||
|
if "description" in update:
|
||||||
|
i["description"] = update["description"].strip()
|
||||||
|
if "category" in update:
|
||||||
|
i["category"] = update["category"]
|
||||||
found = i
|
found = i
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
|
|
@ -1394,6 +1404,33 @@ async def update_issue(issue_id: str, update: dict):
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/issues/{issue_id}/comments")
|
||||||
|
async def add_comment(issue_id: str, comment: dict):
|
||||||
|
"""Add a comment to an issue."""
|
||||||
|
issues = _load_issues()
|
||||||
|
found = None
|
||||||
|
for i in issues:
|
||||||
|
if i.get("id") == issue_id:
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
raise HTTPException(status_code=404, detail="Issue not found")
|
||||||
|
text = comment.get("text", "").strip()
|
||||||
|
if not text:
|
||||||
|
raise HTTPException(status_code=400, detail="Comment text is required")
|
||||||
|
new_comment = {
|
||||||
|
"id": uuid.uuid4().hex[:8],
|
||||||
|
"author": comment.get("author", "Anonymous").strip(),
|
||||||
|
"text": text,
|
||||||
|
"created": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
if "comments" not in found:
|
||||||
|
found["comments"] = []
|
||||||
|
found["comments"].append(new_comment)
|
||||||
|
_save_issues(issues)
|
||||||
|
return new_comment
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/issues/{issue_id}")
|
@app.delete("/issues/{issue_id}")
|
||||||
async def delete_issue(issue_id: str):
|
async def delete_issue(issue_id: str):
|
||||||
"""Permanently delete an issue."""
|
"""Permanently delete an issue."""
|
||||||
|
|
@ -2768,7 +2805,9 @@ async def ws_receive_snapshots(
|
||||||
landblock = data.get("landblock")
|
landblock = data.get("landblock")
|
||||||
if landblock:
|
if landblock:
|
||||||
dungeon_map_cache[landblock] = data
|
dungeon_map_cache[landblock] = data
|
||||||
logger.info(f"Cached dungeon map for {landblock} ({len(data.get('z_levels', []))} z-levels)")
|
logger.info(
|
||||||
|
f"Cached dungeon map for {landblock} ({len(data.get('z_levels', []))} z-levels)"
|
||||||
|
)
|
||||||
await _broadcast_to_browser_clients(data)
|
await _broadcast_to_browser_clients(data)
|
||||||
continue
|
continue
|
||||||
# Unknown message types are ignored
|
# Unknown message types are ignored
|
||||||
|
|
@ -3328,7 +3367,9 @@ class NoCacheStaticFiles(StaticFiles):
|
||||||
# Check content-type header since root path "" resolves to index.html
|
# Check content-type header since root path "" resolves to index.html
|
||||||
# via html=True and we need to catch it too.
|
# via html=True and we need to catch it too.
|
||||||
ct = response.headers.get("content-type", "").lower()
|
ct = response.headers.get("content-type", "").lower()
|
||||||
if any(t in ct for t in ("text/html", "javascript", "text/css", "application/json")):
|
if any(
|
||||||
|
t in ct for t in ("text/html", "javascript", "text/css", "application/json")
|
||||||
|
):
|
||||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
||||||
192
static/script.js
192
static/script.js
|
|
@ -4135,6 +4135,12 @@ const ISSUE_CATEGORIES = {
|
||||||
other: { label: 'Other', color: '#888888' },
|
other: { label: 'Other', color: '#888888' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function showIssuesWindow() {
|
function showIssuesWindow() {
|
||||||
const { win, content, isNew } = createWindow(
|
const { win, content, isNew } = createWindow(
|
||||||
'issuesWindow', 'Issues Board', 'issues-window'
|
'issuesWindow', 'Issues Board', 'issues-window'
|
||||||
|
|
@ -4145,8 +4151,8 @@ function showIssuesWindow() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
win.style.width = '500px';
|
win.style.width = '540px';
|
||||||
win.style.height = '450px';
|
win.style.height = '520px';
|
||||||
|
|
||||||
// Issues list container
|
// Issues list container
|
||||||
const listDiv = document.createElement('div');
|
const listDiv = document.createElement('div');
|
||||||
|
|
@ -4159,6 +4165,7 @@ function showIssuesWindow() {
|
||||||
form.className = 'issues-form';
|
form.className = 'issues-form';
|
||||||
form.innerHTML = `
|
form.innerHTML = `
|
||||||
<div style="display:flex;gap:4px;margin-bottom:4px;">
|
<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;">
|
<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;">
|
<select id="issueCategory" style="padding:3px;font-size:0.75rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
|
||||||
<option value="plugin">Plugin</option>
|
<option value="plugin">Plugin</option>
|
||||||
|
|
@ -4175,17 +4182,24 @@ function showIssuesWindow() {
|
||||||
`;
|
`;
|
||||||
content.appendChild(form);
|
content.appendChild(form);
|
||||||
|
|
||||||
|
// Remember author name in localStorage
|
||||||
|
const authorInput = form.querySelector('#issueAuthor');
|
||||||
|
authorInput.value = localStorage.getItem('issueAuthorName') || '';
|
||||||
|
|
||||||
// Add button handler
|
// Add button handler
|
||||||
form.querySelector('#issueAddBtn').addEventListener('click', async () => {
|
form.querySelector('#issueAddBtn').addEventListener('click', async () => {
|
||||||
|
const author = document.getElementById('issueAuthor').value.trim() || 'Anonymous';
|
||||||
const title = document.getElementById('issueTitle').value.trim();
|
const title = document.getElementById('issueTitle').value.trim();
|
||||||
const desc = document.getElementById('issueDescription').value.trim();
|
const desc = document.getElementById('issueDescription').value.trim();
|
||||||
const cat = document.getElementById('issueCategory').value;
|
const cat = document.getElementById('issueCategory').value;
|
||||||
if (!title) return;
|
if (!title) return;
|
||||||
|
|
||||||
|
localStorage.setItem('issueAuthorName', author);
|
||||||
|
|
||||||
await fetch('/issues', {
|
await fetch('/issues', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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('issueTitle').value = '';
|
||||||
document.getElementById('issueDescription').value = '';
|
document.getElementById('issueDescription').value = '';
|
||||||
|
|
@ -4217,24 +4231,38 @@ async function refreshIssuesList(win) {
|
||||||
const cat = ISSUE_CATEGORIES[issue.category] || ISSUE_CATEGORIES.other;
|
const cat = ISSUE_CATEGORIES[issue.category] || ISSUE_CATEGORIES.other;
|
||||||
const date = issue.created ? new Date(issue.created).toLocaleDateString('sv-SE') : '';
|
const date = issue.created ? new Date(issue.created).toLocaleDateString('sv-SE') : '';
|
||||||
const isResolved = !!issue.resolved;
|
const isResolved = !!issue.resolved;
|
||||||
|
const author = issue.author || 'User';
|
||||||
|
const comments = issue.comments || [];
|
||||||
|
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'issue-row' + (isResolved ? ' issue-resolved' : '');
|
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) {
|
if (isResolved) {
|
||||||
// Reopen button
|
|
||||||
const reopenBtn = document.createElement('button');
|
const reopenBtn = document.createElement('button');
|
||||||
reopenBtn.textContent = '↺ Reopen';
|
reopenBtn.textContent = '\u21BA Reopen';
|
||||||
reopenBtn.className = 'issue-reopen-btn';
|
reopenBtn.className = 'issue-reopen-btn';
|
||||||
reopenBtn.addEventListener('click', async () => {
|
reopenBtn.addEventListener('click', async () => {
|
||||||
await fetch(`/issues/${issue.id}`, {
|
await fetch(`/issues/${issue.id}`, {
|
||||||
|
|
@ -4244,22 +4272,20 @@ async function refreshIssuesList(win) {
|
||||||
});
|
});
|
||||||
refreshIssuesList(win);
|
refreshIssuesList(win);
|
||||||
});
|
});
|
||||||
headerDiv.appendChild(reopenBtn);
|
actionsDiv.appendChild(reopenBtn);
|
||||||
|
|
||||||
// Delete button
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.textContent = '🗑 Delete';
|
deleteBtn.textContent = '\uD83D\uDDD1 Delete';
|
||||||
deleteBtn.className = 'issue-delete-btn';
|
deleteBtn.className = 'issue-delete-btn';
|
||||||
deleteBtn.addEventListener('click', async () => {
|
deleteBtn.addEventListener('click', async () => {
|
||||||
if (!confirm(`Delete issue "${issue.title}"?`)) return;
|
if (!confirm(`Delete issue "${issue.title}"?`)) return;
|
||||||
await fetch(`/issues/${issue.id}`, { method: 'DELETE' });
|
await fetch(`/issues/${issue.id}`, { method: 'DELETE' });
|
||||||
refreshIssuesList(win);
|
refreshIssuesList(win);
|
||||||
});
|
});
|
||||||
headerDiv.appendChild(deleteBtn);
|
actionsDiv.appendChild(deleteBtn);
|
||||||
} else {
|
} else {
|
||||||
// Resolve button (marks as resolved, doesn't delete)
|
|
||||||
const resolveBtn = document.createElement('button');
|
const resolveBtn = document.createElement('button');
|
||||||
resolveBtn.textContent = '✓ Resolve';
|
resolveBtn.textContent = '\u2713 Resolve';
|
||||||
resolveBtn.className = 'issue-resolve-btn';
|
resolveBtn.className = 'issue-resolve-btn';
|
||||||
resolveBtn.addEventListener('click', async () => {
|
resolveBtn.addEventListener('click', async () => {
|
||||||
await fetch(`/issues/${issue.id}`, {
|
await fetch(`/issues/${issue.id}`, {
|
||||||
|
|
@ -4269,12 +4295,136 @@ async function refreshIssuesList(win) {
|
||||||
});
|
});
|
||||||
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);
|
listDiv.appendChild(row);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
listDiv.innerHTML = '<div style="padding:10px;color:#c44;font-size:0.8rem;">Failed to load issues</div>';
|
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 Board ─── */
|
||||||
|
|
||||||
.issues-window {
|
.issues-window {
|
||||||
width: 500px;
|
width: 540px;
|
||||||
height: 450px;
|
height: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.issues-window .window-content {
|
.issues-window .window-content {
|
||||||
|
|
@ -2672,6 +2672,18 @@ table.ts-allegiance td:first-child {
|
||||||
background: #1a1a2a;
|
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 {
|
.issue-row .issue-description {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
@ -2703,15 +2715,22 @@ table.ts-allegiance td:first-child {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.issue-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.issue-resolve-btn,
|
.issue-resolve-btn,
|
||||||
.issue-reopen-btn,
|
.issue-reopen-btn,
|
||||||
.issue-delete-btn {
|
.issue-delete-btn,
|
||||||
|
.issue-edit-btn,
|
||||||
|
.issue-comment-btn {
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin-left: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.issue-resolve-btn {
|
.issue-resolve-btn {
|
||||||
|
|
@ -2744,8 +2763,83 @@ table.ts-allegiance td:first-child {
|
||||||
color: #fff;
|
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 {
|
.issues-form {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-top: 1px solid #444;
|
border-top: 1px solid #444;
|
||||||
background: #1a1a1a;
|
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