Browse Source

support issues

master
Silberengel 4 weeks ago
parent
commit
8ef73f7a62
  1. 168
      src/routes/repos/[naddr]/+page.svelte
  2. 12
      src/routes/write/+page.svelte
  3. 1
      static/changelog.yaml
  4. 4
      static/healthz.json

168
src/routes/repos/[naddr]/+page.svelte

@ -418,7 +418,7 @@
} }
} }
async function loadIssueStatuses() { async function loadIssueStatuses(forceNetwork = false) {
if (issues.length === 0) return; if (issues.length === 0) return;
try { try {
@ -438,6 +438,7 @@
// Status events are different kinds: 1630 (Open), 1631 (Applied/Merged/Resolved), 1632 (Closed), 1633 (Draft) // Status events are different kinds: 1630 (Open), 1631 (Applied/Merged/Resolved), 1632 (Closed), 1633 (Draft)
// They have "e" tags pointing to issues with marker "root" // They have "e" tags pointing to issues with marker "root"
// Use 'relay-first' when forcing network fetch (e.g., after status change) to bypass cached empty results
const statuses = await nostrClient.fetchEvents( const statuses = await nostrClient.fetchEvents(
[{ [{
'#e': issueIds, '#e': issueIds,
@ -445,7 +446,7 @@
limit: 200 limit: 200
}], }],
relays, relays,
{ useCache: 'cache-first', cacheResults: true } // Prioritize cache { useCache: forceNetwork ? 'relay-first' : 'cache-first', cacheResults: true }
); );
// Process statuses for this batch // Process statuses for this batch
@ -486,8 +487,18 @@
} }
} }
// Merge with existing state to preserve any local updates that haven't propagated yet
// Keep the newer status for each issue
const mergedStatuses = new Map(issueStatuses);
for (const [issueId, newStatus] of statusMap.entries()) {
const existingStatus = mergedStatuses.get(issueId);
if (!existingStatus || newStatus.created_at > existingStatus.created_at) {
mergedStatuses.set(issueId, newStatus);
}
}
// Update state after each batch so newest issues show statuses first // Update state after each batch so newest issues show statuses first
issueStatuses = new Map(statusMap); issueStatuses = mergedStatuses;
} }
} catch (error) { } catch (error) {
// Failed to load issue statuses // Failed to load issue statuses
@ -552,6 +563,12 @@
issueStatuses.set(issueId, signedEvent); issueStatuses.set(issueId, signedEvent);
issueStatuses = new Map(issueStatuses); // Trigger reactivity issueStatuses = new Map(issueStatuses); // Trigger reactivity
// Refresh the view immediately using cache-first to show the newly cached event
// This ensures the cached event is displayed right away
loadIssueStatuses(false).catch(() => {
// Non-critical - status is already updated locally
});
// Publish to relays in background (don't wait for it) // Publish to relays in background (don't wait for it)
const relays = relayManager.getProfileReadRelays(); const relays = relayManager.getProfileReadRelays();
signAndPublish(event, relays).then((result) => { signAndPublish(event, relays).then((result) => {
@ -559,10 +576,13 @@
console.warn('Failed to publish status change to some relays:', result.failed); console.warn('Failed to publish status change to some relays:', result.failed);
// Don't show alert - event is cached and will sync eventually // Don't show alert - event is cached and will sync eventually
} }
// Reload statuses to get any other updates from relays // Optionally reload from network to get any other updates from relays
loadIssueStatuses().catch(() => { // Use a small delay to allow the event to propagate
// Non-critical - status is already updated locally setTimeout(() => {
}); loadIssueStatuses(true).catch(() => {
// Non-critical - status is already updated locally
});
}, 1000);
}).catch((error) => { }).catch((error) => {
console.error('Error publishing status change:', error); console.error('Error publishing status change:', error);
// Don't show alert - event is cached and will sync eventually // Don't show alert - event is cached and will sync eventually
@ -854,6 +874,15 @@
} }
} }
function getRepoATag(): string | null {
if (!repoEvent) return null;
const dTag = repoEvent.tags.find(t => Array.isArray(t) && t[0] === 'd')?.[1] || '';
if (dTag) {
return `${repoEvent.kind}:${repoEvent.pubkey}:${dTag}`;
}
return null;
}
function getRepoName(): string { function getRepoName(): string {
try { try {
if (!repoEvent) return 'Repository'; if (!repoEvent) return 'Repository';
@ -1470,31 +1499,40 @@
</div> </div>
{:else if issues.length > 0} {:else if issues.length > 0}
<div class="issues-filter"> <div class="issues-filter">
<label for="status-filter" class="filter-label">Filter by status:</label> <div class="issues-filter-left">
<select <label for="status-filter" class="filter-label">Filter by status:</label>
id="status-filter" <select
value={statusFilter || 'all'} id="status-filter"
onchange={(e) => { value={statusFilter || 'all'}
const value = (e.target as HTMLSelectElement).value; onchange={(e) => {
statusFilter = value === 'all' ? null : value; const value = (e.target as HTMLSelectElement).value;
}} statusFilter = value === 'all' ? null : value;
class="status-filter-select" }}
> class="status-filter-select"
<option value="all">All</option> >
{#each availableStatuses as statusOption} <option value="all">All</option>
<option value={statusOption}>{statusOption}</option> {#each availableStatuses as statusOption}
{/each} <option value={statusOption}>{statusOption}</option>
</select> {/each}
<span class="filter-count"> </select>
{#if statusFilter} <span class="filter-count">
Showing {filteredIssues.length} of {issues.length} issues {#if statusFilter}
{:else} Showing {filteredIssues.length} of {issues.length} issues
{issues.length} {issues.length === 1 ? 'issue' : 'issues'} {:else}
{/if} {issues.length} {issues.length === 1 ? 'issue' : 'issues'}
{#if loadingIssueData} {/if}
<span class="loading-indicator"> (loading details...)</span> {#if loadingIssueData}
{/if} <span class="loading-indicator"> (loading details...)</span>
</span> {/if}
</span>
</div>
{#if sessionManager.getSession()}
{@const repoATag = getRepoATag()}
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue">
<Icon name="edit" size={16} />
<span>New Issue</span>
</a>
{/if}
</div> </div>
<div class="issues-list"> <div class="issues-list">
{#if filteredIssues.length > 0} {#if filteredIssues.length > 0}
@ -1577,12 +1615,26 @@
{:else} {:else}
<div class="empty-state"> <div class="empty-state">
<p class="text-fog-text dark:text-fog-dark-text">No issues found with status "{statusFilter}".</p> <p class="text-fog-text dark:text-fog-dark-text">No issues found with status "{statusFilter}".</p>
{#if sessionManager.getSession()}
{@const repoATag = getRepoATag()}
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue">
<Icon name="edit" size={16} />
<span>New Issue</span>
</a>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
{:else} {:else}
<div class="empty-state"> <div class="empty-state">
<p class="text-fog-text dark:text-fog-dark-text">No issues found.</p> <p class="text-fog-text dark:text-fog-dark-text">No issues found.</p>
{#if sessionManager.getSession()}
{@const repoATag = getRepoATag()}
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue">
<Icon name="edit" size={16} />
<span>New Issue</span>
</a>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@ -1634,6 +1686,10 @@
.empty-state { .empty-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
} }
.repo-banner-container { .repo-banner-container {
@ -1986,12 +2042,14 @@
.issues-filter { .issues-filter {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 1rem; gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding: 1rem; padding: 1rem;
background: var(--fog-highlight, #f3f4f6); background: var(--fog-highlight, #f3f4f6);
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb); border: 1px solid var(--fog-border, #e5e7eb);
flex-wrap: wrap;
} }
:global(.dark) .issues-filter { :global(.dark) .issues-filter {
@ -1999,6 +2057,14 @@
border-color: var(--fog-dark-border, #374151); border-color: var(--fog-dark-border, #374151);
} }
.issues-filter-left {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
flex: 1;
}
.filter-label { .filter-label {
font-weight: 500; font-weight: 500;
color: var(--fog-text, #1f2937); color: var(--fog-text, #1f2937);
@ -2046,7 +2112,6 @@
} }
.filter-count { .filter-count {
margin-left: auto;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--fog-text-light, #52667a); color: var(--fog-text-light, #52667a);
} }
@ -2055,6 +2120,43 @@
color: var(--fog-dark-text-light, #a8b8d0); color: var(--fog-dark-text-light, #a8b8d0);
} }
.see-more-events-btn-header {
padding: 0.5rem 1rem;
border: 1px solid var(--fog-border, #cbd5e1);
border-radius: 4px;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1e293b);
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.see-more-events-btn-header:hover:not(:disabled) {
background: var(--fog-highlight, #f1f5f9);
border-color: var(--fog-accent, #94a3b8);
}
.see-more-events-btn-header:disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.dark) .see-more-events-btn-header {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .see-more-events-btn-header:hover:not(:disabled) {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #64748b);
}
.issues-list { .issues-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

12
src/routes/write/+page.svelte

@ -9,8 +9,9 @@
import type { NostrEvent } from '../../lib/types/nostr.js'; import type { NostrEvent } from '../../lib/types/nostr.js';
// Read kind from URL synchronously so it's available on first render // Read kind and a-tag from URL synchronously so it's available on first render
const kindParam = $derived($page.url.searchParams.get('kind')); const kindParam = $derived($page.url.searchParams.get('kind'));
const aTagParam = $derived($page.url.searchParams.get('a'));
let initialEvent = $state<NostrEvent | null>(null); let initialEvent = $state<NostrEvent | null>(null);
let isCloneMode = $state(false); let isCloneMode = $state(false);
@ -65,18 +66,23 @@
} }
} }
// Set initial kind from URL if available (only if no event from sessionStorage) // Set initial kind and tags from URL if available (only if no event from sessionStorage)
$effect(() => { $effect(() => {
if (kindParam && !initialEvent) { if (kindParam && !initialEvent) {
const kind = parseInt(kindParam, 10); const kind = parseInt(kindParam, 10);
if (!isNaN(kind)) { if (!isNaN(kind)) {
const tags: string[][] = [];
// Add a-tag if provided (for repo references in issues)
if (aTagParam) {
tags.push(['a', aTagParam]);
}
initialEvent = { initialEvent = {
id: '', id: '',
pubkey: '', pubkey: '',
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: kind, kind: kind,
content: '', content: '',
tags: [], tags: tags,
sig: '' sig: ''
}; };
} }

1
static/changelog.yaml

@ -2,6 +2,7 @@ versions:
'0.3.3': '0.3.3':
- 'Added GRASP repository management' - 'Added GRASP repository management'
- 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)' - 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)'
- 'Support creation of issues in repositories'
'0.3.2': '0.3.2':
- 'Expanded /repos to handle GitLab, Gitea, and OneDev repositories' - 'Expanded /repos to handle GitLab, Gitea, and OneDev repositories'
- 'Added back and refresh buttons to all pages' - 'Added back and refresh buttons to all pages'

4
static/healthz.json

@ -2,7 +2,7 @@
"status": "ok", "status": "ok",
"service": "aitherboard", "service": "aitherboard",
"version": "0.3.3", "version": "0.3.3",
"buildTime": "2026-02-15T06:43:22.492Z", "buildTime": "2026-02-15T12:37:12.663Z",
"gitCommit": "unknown", "gitCommit": "unknown",
"timestamp": 1771137802493 "timestamp": 1771159032663
} }
Loading…
Cancel
Save