|
|
|
@ -314,13 +314,16 @@ |
|
|
|
let announcementEventId = $state<string | null>(null); |
|
|
|
let announcementEventId = $state<string | null>(null); |
|
|
|
|
|
|
|
|
|
|
|
// Issues |
|
|
|
// Issues |
|
|
|
let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; kind: number }>>([]); |
|
|
|
let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; kind: number; tags?: string[][] }>>([]); |
|
|
|
let loadingIssues = $state(false); |
|
|
|
let loadingIssues = $state(false); |
|
|
|
let showCreateIssueDialog = $state(false); |
|
|
|
let showCreateIssueDialog = $state(false); |
|
|
|
let newIssueSubject = $state(''); |
|
|
|
let newIssueSubject = $state(''); |
|
|
|
let newIssueContent = $state(''); |
|
|
|
let newIssueContent = $state(''); |
|
|
|
let newIssueLabels = $state<string[]>(['']); |
|
|
|
let newIssueLabels = $state<string[]>(['']); |
|
|
|
let updatingIssueStatus = $state<Record<string, boolean>>({}); |
|
|
|
let updatingIssueStatus = $state<Record<string, boolean>>({}); |
|
|
|
|
|
|
|
let selectedIssue = $state<string | null>(null); |
|
|
|
|
|
|
|
let issueReplies = $state<Array<{ id: string; content: string; author: string; created_at: number; tags: string[][] }>>([]); |
|
|
|
|
|
|
|
let loadingIssueReplies = $state(false); |
|
|
|
|
|
|
|
|
|
|
|
// Pull Requests |
|
|
|
// Pull Requests |
|
|
|
let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string; kind: number }>>([]); |
|
|
|
let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string; kind: number }>>([]); |
|
|
|
@ -370,7 +373,7 @@ |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Patches |
|
|
|
// Patches |
|
|
|
let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number }>>([]); |
|
|
|
let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number; description?: string; tags?: string[][] }>>([]); |
|
|
|
let loadingPatches = $state(false); |
|
|
|
let loadingPatches = $state(false); |
|
|
|
let selectedPatch = $state<string | null>(null); |
|
|
|
let selectedPatch = $state<string | null>(null); |
|
|
|
let showCreatePatchDialog = $state(false); |
|
|
|
let showCreatePatchDialog = $state(false); |
|
|
|
@ -3907,6 +3910,9 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function viewDiff(commitHash: string) { |
|
|
|
async function viewDiff(commitHash: string) { |
|
|
|
|
|
|
|
// Set selected commit immediately so it shows in the right panel |
|
|
|
|
|
|
|
selectedCommit = commitHash; |
|
|
|
|
|
|
|
showDiff = false; // Start with false, will be set to true when diff loads |
|
|
|
loadingCommits = true; |
|
|
|
loadingCommits = true; |
|
|
|
error = null; |
|
|
|
error = null; |
|
|
|
try { |
|
|
|
try { |
|
|
|
@ -3922,7 +3928,6 @@ |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (response.ok) { |
|
|
|
if (response.ok) { |
|
|
|
diffData = await response.json(); |
|
|
|
diffData = await response.json(); |
|
|
|
selectedCommit = commitHash; |
|
|
|
|
|
|
|
showDiff = true; |
|
|
|
showDiff = true; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
@ -4008,8 +4013,14 @@ |
|
|
|
status: issue.status || 'open', |
|
|
|
status: issue.status || 'open', |
|
|
|
author: issue.pubkey, |
|
|
|
author: issue.pubkey, |
|
|
|
created_at: issue.created_at, |
|
|
|
created_at: issue.created_at, |
|
|
|
kind: issue.kind || KIND.ISSUE |
|
|
|
kind: issue.kind || KIND.ISSUE, |
|
|
|
|
|
|
|
tags: issue.tags || [] |
|
|
|
})); |
|
|
|
})); |
|
|
|
|
|
|
|
// Auto-select first issue if none selected |
|
|
|
|
|
|
|
if (issues.length > 0 && !selectedIssue) { |
|
|
|
|
|
|
|
selectedIssue = issues[0].id; |
|
|
|
|
|
|
|
loadIssueReplies(issues[0].id); |
|
|
|
|
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// Handle non-OK responses |
|
|
|
// Handle non-OK responses |
|
|
|
const errorText = await response.text().catch(() => response.statusText); |
|
|
|
const errorText = await response.text().catch(() => response.statusText); |
|
|
|
@ -4039,6 +4050,32 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadIssueReplies(issueId: string) { |
|
|
|
|
|
|
|
loadingIssueReplies = true; |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const replies = await nostrClient.fetchEvents([ |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
kinds: [KIND.COMMENT], |
|
|
|
|
|
|
|
'#e': [issueId], |
|
|
|
|
|
|
|
limit: 100 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
]) as NostrEvent[]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
issueReplies = replies.map(reply => ({ |
|
|
|
|
|
|
|
id: reply.id, |
|
|
|
|
|
|
|
content: reply.content, |
|
|
|
|
|
|
|
author: reply.pubkey, |
|
|
|
|
|
|
|
created_at: reply.created_at, |
|
|
|
|
|
|
|
tags: reply.tags || [] |
|
|
|
|
|
|
|
})).sort((a, b) => a.created_at - b.created_at); |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
console.error('[Issues] Error loading replies:', err); |
|
|
|
|
|
|
|
issueReplies = []; |
|
|
|
|
|
|
|
} finally { |
|
|
|
|
|
|
|
loadingIssueReplies = false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function createIssue() { |
|
|
|
async function createIssue() { |
|
|
|
if (!newIssueSubject.trim() || !newIssueContent.trim()) { |
|
|
|
if (!newIssueSubject.trim() || !newIssueContent.trim()) { |
|
|
|
alert('Please enter a subject and content'); |
|
|
|
alert('Please enter a subject and content'); |
|
|
|
@ -4296,14 +4333,45 @@ |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (response.ok) { |
|
|
|
if (response.ok) { |
|
|
|
const data = await response.json(); |
|
|
|
const data = await response.json(); |
|
|
|
patches = data.map((patch: { id: string; tags: string[][]; content: string; pubkey: string; created_at: number; kind?: number }) => ({ |
|
|
|
patches = data.map((patch: { id: string; tags: string[][]; content: string; pubkey: string; created_at: number; kind?: number }) => { |
|
|
|
|
|
|
|
// Extract subject/title from various sources |
|
|
|
|
|
|
|
let subject = patch.tags.find((t: string[]) => t[0] === 'subject')?.[1]; |
|
|
|
|
|
|
|
const description = patch.tags.find((t: string[]) => t[0] === 'description')?.[1]; |
|
|
|
|
|
|
|
const alt = patch.tags.find((t: string[]) => t[0] === 'alt')?.[1]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If no subject tag, try description or alt |
|
|
|
|
|
|
|
if (!subject) { |
|
|
|
|
|
|
|
if (description) { |
|
|
|
|
|
|
|
subject = description.trim(); |
|
|
|
|
|
|
|
} else if (alt) { |
|
|
|
|
|
|
|
// Remove "git patch: " prefix if present |
|
|
|
|
|
|
|
subject = alt.replace(/^git patch:\s*/i, '').trim(); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Try to extract from patch content (git patch format) |
|
|
|
|
|
|
|
const subjectMatch = patch.content.match(/^Subject:\s*\[PATCH[^\]]*\]\s*(.+)$/m); |
|
|
|
|
|
|
|
if (subjectMatch) { |
|
|
|
|
|
|
|
subject = subjectMatch[1].trim(); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Try simpler Subject: line |
|
|
|
|
|
|
|
const simpleSubjectMatch = patch.content.match(/^Subject:\s*(.+)$/m); |
|
|
|
|
|
|
|
if (simpleSubjectMatch) { |
|
|
|
|
|
|
|
subject = simpleSubjectMatch[1].trim(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
id: patch.id, |
|
|
|
id: patch.id, |
|
|
|
subject: patch.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', |
|
|
|
subject: subject || 'Untitled', |
|
|
|
content: patch.content, |
|
|
|
content: patch.content, |
|
|
|
author: patch.pubkey, |
|
|
|
author: patch.pubkey, |
|
|
|
created_at: patch.created_at, |
|
|
|
created_at: patch.created_at, |
|
|
|
kind: patch.kind || KIND.PATCH |
|
|
|
kind: patch.kind || KIND.PATCH, |
|
|
|
})); |
|
|
|
description: description?.trim(), |
|
|
|
|
|
|
|
tags: patch.tags || [] |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
error = err instanceof Error ? err.message : 'Failed to load patches'; |
|
|
|
error = err instanceof Error ? err.message : 'Failed to load patches'; |
|
|
|
@ -4835,22 +4903,6 @@ |
|
|
|
<img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" /> |
|
|
|
<img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" /> |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{#if tags.length > 0} |
|
|
|
|
|
|
|
<ul class="tag-list"> |
|
|
|
|
|
|
|
{#each tags as tag} |
|
|
|
|
|
|
|
{@const tagHash = tag.hash || ''} |
|
|
|
|
|
|
|
{#if tagHash} |
|
|
|
|
|
|
|
<li class="tag-item"> |
|
|
|
|
|
|
|
<div class="tag-name">{tag.name}</div> |
|
|
|
|
|
|
|
<div class="tag-hash">{tagHash.slice(0, 7)}</div> |
|
|
|
|
|
|
|
{#if tag.message} |
|
|
|
|
|
|
|
<div class="tag-message">{tag.message}</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</li> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
{/each} |
|
|
|
|
|
|
|
</ul> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</aside> |
|
|
|
</aside> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
@ -4885,34 +4937,21 @@ |
|
|
|
{:else if issues.length > 0} |
|
|
|
{:else if issues.length > 0} |
|
|
|
<ul class="issue-list"> |
|
|
|
<ul class="issue-list"> |
|
|
|
{#each issues as issue} |
|
|
|
{#each issues as issue} |
|
|
|
<li class="issue-item"> |
|
|
|
<li class="issue-item" class:selected={selectedIssue === issue.id}> |
|
|
|
|
|
|
|
<button |
|
|
|
|
|
|
|
onclick={() => { |
|
|
|
|
|
|
|
selectedIssue = issue.id; |
|
|
|
|
|
|
|
loadIssueReplies(issue.id); |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
class="issue-item-button" |
|
|
|
|
|
|
|
> |
|
|
|
<div class="issue-header"> |
|
|
|
<div class="issue-header"> |
|
|
|
<span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}> |
|
|
|
<span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}> |
|
|
|
{issue.status} |
|
|
|
{issue.status} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
<span class="issue-subject">{issue.subject}</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="issue-meta"> |
|
|
|
|
|
|
|
<span>#{issue.id.slice(0, 7)}</span> |
|
|
|
|
|
|
|
<span>{new Date(issue.created_at * 1000).toLocaleDateString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={issue.id} kind={issue.kind} pubkey={issue.author} /> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{#if userPubkeyHex && (isMaintainer || userPubkeyHex === issue.author)} |
|
|
|
<div class="issue-subject">{issue.subject}</div> |
|
|
|
<div class="issue-actions"> |
|
|
|
|
|
|
|
{#if issue.status === 'open'} |
|
|
|
|
|
|
|
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'closed')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn close-btn"> |
|
|
|
|
|
|
|
{updatingIssueStatus[issue.id] ? 'Closing...' : 'Close'} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'resolved')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn resolve-btn"> |
|
|
|
|
|
|
|
{updatingIssueStatus[issue.id] ? 'Resolving...' : 'Resolve'} |
|
|
|
|
|
|
|
</button> |
|
|
|
</button> |
|
|
|
{:else if issue.status === 'closed' || issue.status === 'resolved'} |
|
|
|
|
|
|
|
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'open')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn reopen-btn"> |
|
|
|
|
|
|
|
{updatingIssueStatus[issue.id] ? 'Reopening...' : 'Reopen'} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</li> |
|
|
|
</li> |
|
|
|
{/each} |
|
|
|
{/each} |
|
|
|
</ul> |
|
|
|
</ul> |
|
|
|
@ -5286,7 +5325,8 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
{#if activeTab === 'history' && showDiff} |
|
|
|
{#if activeTab === 'history'} |
|
|
|
|
|
|
|
<div class="commits-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'history'}> |
|
|
|
<div class="content-header-mobile"> |
|
|
|
<div class="content-header-mobile"> |
|
|
|
<button |
|
|
|
<button |
|
|
|
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} |
|
|
|
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} |
|
|
|
@ -5296,11 +5336,23 @@ |
|
|
|
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> |
|
|
|
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="diff-view"> |
|
|
|
{#if selectedCommit} |
|
|
|
<div class="diff-header"> |
|
|
|
{@const commit = commits.find(c => (c.hash || (c as any).sha) === selectedCommit)} |
|
|
|
<h3>Diff for commit {selectedCommit?.slice(0, 7)}</h3> |
|
|
|
{#if commit} |
|
|
|
|
|
|
|
<div class="commit-detail"> |
|
|
|
|
|
|
|
<div class="commit-detail-header"> |
|
|
|
|
|
|
|
<h3>{commit.message || 'No message'}</h3> |
|
|
|
<button onclick={() => { showDiff = false; selectedCommit = null; }} class="close-button">×</button> |
|
|
|
<button onclick={() => { showDiff = false; selectedCommit = null; }} class="close-button">×</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="commit-meta-detail"> |
|
|
|
|
|
|
|
<span>#{selectedCommit.slice(0, 7)}</span> |
|
|
|
|
|
|
|
<span>{commit.author || 'Unknown'}</span> |
|
|
|
|
|
|
|
<span>{commit.date ? new Date(commit.date).toLocaleString() : 'Unknown date'}</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{#if loadingCommits} |
|
|
|
|
|
|
|
<div class="loading">Loading diff...</div> |
|
|
|
|
|
|
|
{:else if showDiff && diffData.length > 0} |
|
|
|
|
|
|
|
<div class="diff-view"> |
|
|
|
{#each diffData as diff} |
|
|
|
{#each diffData as diff} |
|
|
|
<div class="diff-file"> |
|
|
|
<div class="diff-file"> |
|
|
|
<div class="diff-file-header"> |
|
|
|
<div class="diff-file-header"> |
|
|
|
@ -5314,22 +5366,27 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/each} |
|
|
|
{/each} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{:else if activeTab === 'history'} |
|
|
|
{:else if showDiff} |
|
|
|
<div class="content-header-mobile"> |
|
|
|
<div class="empty-state"> |
|
|
|
<button |
|
|
|
<p>No diff data available</p> |
|
|
|
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} |
|
|
|
|
|
|
|
class="mobile-toggle-button" |
|
|
|
|
|
|
|
title="Show list" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{:else} |
|
|
|
<div class="empty-state"> |
|
|
|
<div class="empty-state"> |
|
|
|
<p>Select a commit to view its diff</p> |
|
|
|
<p>Loading diff...</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
{:else} |
|
|
|
|
|
|
|
<div class="empty-state"> |
|
|
|
|
|
|
|
<p>Select a commit from the sidebar to view details</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
{#if activeTab === 'tags'} |
|
|
|
{#if activeTab === 'tags'} |
|
|
|
|
|
|
|
<div class="tags-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'tags'}> |
|
|
|
<div class="content-header-mobile"> |
|
|
|
<div class="content-header-mobile"> |
|
|
|
<button |
|
|
|
<button |
|
|
|
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} |
|
|
|
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} |
|
|
|
@ -5339,8 +5396,26 @@ |
|
|
|
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> |
|
|
|
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{#if tags.length > 0} |
|
|
|
|
|
|
|
<ul class="tag-list"> |
|
|
|
|
|
|
|
{#each tags as tag} |
|
|
|
|
|
|
|
{@const tagHash = tag.hash || ''} |
|
|
|
|
|
|
|
{#if tagHash} |
|
|
|
|
|
|
|
<li class="tag-item"> |
|
|
|
|
|
|
|
<div class="tag-name">{tag.name}</div> |
|
|
|
|
|
|
|
<div class="tag-hash">{tagHash.slice(0, 7)}</div> |
|
|
|
|
|
|
|
{#if tag.message} |
|
|
|
|
|
|
|
<div class="tag-message">{tag.message}</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</li> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
{/each} |
|
|
|
|
|
|
|
</ul> |
|
|
|
|
|
|
|
{:else} |
|
|
|
<div class="empty-state"> |
|
|
|
<div class="empty-state"> |
|
|
|
<p>Tags are displayed in the sidebar</p> |
|
|
|
<p>No tags found</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
@ -5368,23 +5443,71 @@ |
|
|
|
<div class="empty-state"> |
|
|
|
<div class="empty-state"> |
|
|
|
<p>No issues found. Create one to get started!</p> |
|
|
|
<p>No issues found. Create one to get started!</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{:else} |
|
|
|
{:else if selectedIssue} |
|
|
|
{#each issues as issue} |
|
|
|
{@const issue = issues.find(i => i.id === selectedIssue)} |
|
|
|
|
|
|
|
{#if issue} |
|
|
|
<div class="issue-detail"> |
|
|
|
<div class="issue-detail"> |
|
|
|
<h3>{issue.subject}</h3> |
|
|
|
<h3>{issue.subject}</h3> |
|
|
|
<div class="issue-meta-detail"> |
|
|
|
<div class="issue-meta-detail"> |
|
|
|
<span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}> |
|
|
|
<span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}> |
|
|
|
{issue.status} |
|
|
|
{issue.status} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
|
|
|
|
<span>#{issue.id.slice(0, 7)}</span> |
|
|
|
<span>Created {new Date(issue.created_at * 1000).toLocaleString()}</span> |
|
|
|
<span>Created {new Date(issue.created_at * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={issue.id} kind={issue.kind} pubkey={issue.author} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="issue-body"> |
|
|
|
<div class="issue-body"> |
|
|
|
{@html issue.content.replace(/\n/g, '<br>')} |
|
|
|
{@html issue.content.replace(/\n/g, '<br>')} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{#if userPubkeyHex && (isMaintainer || userPubkeyHex === issue.author)} |
|
|
|
|
|
|
|
<div class="issue-actions"> |
|
|
|
|
|
|
|
{#if issue.status === 'open'} |
|
|
|
|
|
|
|
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'closed')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn close-btn"> |
|
|
|
|
|
|
|
{updatingIssueStatus[issue.id] ? 'Closing...' : 'Close'} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'resolved')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn resolve-btn"> |
|
|
|
|
|
|
|
{updatingIssueStatus[issue.id] ? 'Resolving...' : 'Resolve'} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
{:else if issue.status === 'closed' || issue.status === 'resolved'} |
|
|
|
|
|
|
|
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'open')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn reopen-btn"> |
|
|
|
|
|
|
|
{updatingIssueStatus[issue.id] ? 'Reopening...' : 'Reopen'} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="issue-replies"> |
|
|
|
|
|
|
|
<h4>Replies ({issueReplies.length})</h4> |
|
|
|
|
|
|
|
{#if loadingIssueReplies} |
|
|
|
|
|
|
|
<div class="loading">Loading replies...</div> |
|
|
|
|
|
|
|
{:else if issueReplies.length === 0} |
|
|
|
|
|
|
|
<div class="empty-state"> |
|
|
|
|
|
|
|
<p>No replies yet.</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{:else} |
|
|
|
|
|
|
|
{#each issueReplies as reply} |
|
|
|
|
|
|
|
<div class="issue-reply"> |
|
|
|
|
|
|
|
<div class="reply-header"> |
|
|
|
|
|
|
|
<UserBadge pubkey={reply.author} /> |
|
|
|
|
|
|
|
<span class="reply-date">{new Date(reply.created_at * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={reply.id} kind={KIND.COMMENT} pubkey={reply.author} /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="reply-body"> |
|
|
|
|
|
|
|
{@html reply.content.replace(/\n/g, '<br>')} |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/each} |
|
|
|
{/each} |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
{:else} |
|
|
|
|
|
|
|
<div class="empty-state"> |
|
|
|
|
|
|
|
<p>Select an issue to view details</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</div> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
{#if activeTab === 'prs'} |
|
|
|
{#if activeTab === 'prs'} |
|
|
|
@ -5476,6 +5599,9 @@ |
|
|
|
<span>Created {new Date(patch.created_at * 1000).toLocaleString()}</span> |
|
|
|
<span>Created {new Date(patch.created_at * 1000).toLocaleString()}</span> |
|
|
|
<EventCopyButton eventId={patch.id} kind={patch.kind} pubkey={patch.author} /> |
|
|
|
<EventCopyButton eventId={patch.id} kind={patch.kind} pubkey={patch.author} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{#if patch.description && patch.description !== patch.subject} |
|
|
|
|
|
|
|
<div class="patch-description">{patch.description}</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
<div class="patch-body"> |
|
|
|
<div class="patch-body"> |
|
|
|
<pre class="patch-content">{patch.content}</pre> |
|
|
|
<pre class="patch-content">{patch.content}</pre> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|