You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
4.4 KiB
184 lines
4.4 KiB
<script lang="ts"> |
|
/** |
|
* Pull Requests tab component using hierarchical layout |
|
*/ |
|
|
|
import StatusTabLayout from './StatusTabLayout.svelte'; |
|
import { renderContent } from '../utils/content-renderer.js'; |
|
import { onMount } from 'svelte'; |
|
|
|
interface Props { |
|
prs: Array<{ |
|
id: string; |
|
subject: string; |
|
content: string; |
|
status: string; |
|
author: string; |
|
created_at: number; |
|
commitId?: string; |
|
kind: number; |
|
}>; |
|
selectedPR?: string | null; |
|
loading?: boolean; |
|
error?: string | null; |
|
onSelect?: (id: string) => void; |
|
onStatusUpdate?: (id: string, status: string) => void; |
|
activeTab?: string; |
|
tabs?: Array<{ id: string; label: string; icon?: string }>; |
|
onTabChange?: (tab: string) => void; |
|
onCreate?: () => void; |
|
userPubkey?: string | null; |
|
} |
|
|
|
let { |
|
prs = [], |
|
selectedPR = null, |
|
loading = false, |
|
error = null, |
|
onSelect = () => {}, |
|
onStatusUpdate = () => {}, |
|
activeTab = '', |
|
tabs = [], |
|
onTabChange = () => {}, |
|
onCreate, |
|
userPubkey = null |
|
}: Props = $props(); |
|
|
|
const items = $derived(prs.map(pr => ({ |
|
...pr, |
|
id: pr.id, |
|
title: pr.subject, |
|
status: pr.status || 'open' |
|
}))); |
|
|
|
const selectedId = $derived(selectedPR); |
|
|
|
// Cache for rendered content |
|
let renderedContent = $state<Map<string, string>>(new Map()); |
|
|
|
async function getRenderedContent(content: string, kind?: number): Promise<string> { |
|
if (!content) return 'No content'; |
|
const cacheKey = `${kind || 'markdown'}:${content.slice(0, 50)}`; |
|
if (renderedContent.has(cacheKey)) { |
|
return renderedContent.get(cacheKey)!; |
|
} |
|
const rendered = await renderContent(content, kind); |
|
renderedContent.set(cacheKey, rendered); |
|
return rendered; |
|
} |
|
</script> |
|
|
|
{#snippet itemRenderer({ item }: { item: any })} |
|
<div class="pr-item-content"> |
|
<div class="pr-subject">{item.subject}</div> |
|
<div class="pr-meta"> |
|
<span class="pr-id">#{item.id.slice(0, 7)}</span> |
|
{#if item.commitId} |
|
<span class="pr-commit">Commit: {item.commitId.slice(0, 7)}</span> |
|
{/if} |
|
<span class="pr-date">{new Date(item.created_at * 1000).toLocaleDateString()}</span> |
|
</div> |
|
</div> |
|
{/snippet} |
|
|
|
{#snippet detailRenderer({ item }: { item: any })} |
|
{@const contentPromise = getRenderedContent(item.content || '', item.kind)} |
|
{@const currentStatus = item.status || 'open'} |
|
<div class="pr-detail"> |
|
<div class="pr-detail-header"> |
|
<h2>{item.subject}</h2> |
|
<div class="pr-actions"> |
|
<select |
|
value={currentStatus} |
|
onchange={(e) => onStatusUpdate(item.id, (e.target as HTMLSelectElement).value)} |
|
> |
|
<option value="open">Open</option> |
|
<option value="closed">Closed</option> |
|
<option value="merged">Merged</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
<div class="pr-content"> |
|
{#await contentPromise} |
|
<div class="loading">Rendering content...</div> |
|
{:then html} |
|
{@html html} |
|
{:catch err} |
|
<div class="error">Failed to render content: {err instanceof Error ? err.message : String(err)}</div> |
|
{/await} |
|
</div> |
|
|
|
{#if item.commitId} |
|
<div class="pr-commit-info"> |
|
<strong>Commit:</strong> {item.commitId} |
|
</div> |
|
{/if} |
|
</div> |
|
{/snippet} |
|
|
|
<StatusTabLayout |
|
{items} |
|
{selectedId} |
|
{loading} |
|
{error} |
|
{onSelect} |
|
statusGroups={[ |
|
{ label: 'Open', value: 'open' }, |
|
{ label: 'Closed', value: 'closed' }, |
|
{ label: 'Merged', value: 'merged' } |
|
]} |
|
{itemRenderer} |
|
{detailRenderer} |
|
{activeTab} |
|
{tabs} |
|
{onTabChange} |
|
title="Pull Requests" |
|
{onCreate} |
|
showCreateButton={!!userPubkey && !!onCreate} |
|
/> |
|
|
|
<style> |
|
.pr-item-content { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.25rem; |
|
} |
|
|
|
.pr-subject { |
|
font-weight: 500; |
|
} |
|
|
|
.pr-meta { |
|
display: flex; |
|
gap: 0.5rem; |
|
font-size: 0.85rem; |
|
color: var(--text-secondary); |
|
} |
|
|
|
.pr-detail { |
|
padding: 1rem; |
|
} |
|
|
|
.pr-detail-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 1rem; |
|
padding-bottom: 1rem; |
|
border-bottom: 1px solid var(--border-color); |
|
} |
|
|
|
.pr-content { |
|
margin: 1rem 0; |
|
line-height: 1.6; |
|
} |
|
|
|
.pr-commit-info { |
|
margin-top: 1rem; |
|
padding: 1rem; |
|
background: var(--bg-secondary); |
|
border-radius: 4px; |
|
font-family: monospace; |
|
} |
|
</style>
|
|
|