Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions src/features/pr-review/PRDescription.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,12 @@

const cleanText = $derived.by(() => stripMarkdown(displayText));
const renderedMarkdown = $derived.by(() => {
// Always render markdown when expanded, or when there's no toggle needed
if ((expanded && shouldShowToggle) || !shouldShowToggle) {
try {
return marked.parse(displayText);
} catch (error) {
console.error('Error rendering markdown:', error);
return displayText;
}
try {
return marked.parse(displayText);
} catch (error) {
console.error('Error rendering markdown:', error);
return displayText;
}
return null;
});

// Check if the content has markdown formatting
Expand All @@ -84,13 +80,11 @@
</script>

<div class="mt-3">
{#if hasMarkdown && (renderedMarkdown || (!shouldShowToggle && !expanded))}
<!-- Render markdown when there's markdown content -->
{#if hasMarkdown}
<div class="gh-markdown prose prose-sm max-w-none prose-invert">
{@html renderedMarkdown}
</div>
{:else}
<!-- Show plain text when no markdown or in preview mode -->
<div class="text-sm text-[#c9d1d9] whitespace-pre-line">
{cleanText}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@
</div>

<!-- Action buttons -->
<div class="flex items-center justify-end gap-2">
<div class="flex gap-2">
<button
onclick={() => onSubmitReview && onSubmitReview('REQUEST_CHANGES')}
disabled={!canSubmit || !canRequestChanges}
class="px-3 py-2 text-sm bg-[#da3633] text-white rounded font-medium hover:bg-[#f85149] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
class="flex-[2] px-3 py-2 text-sm bg-[#da3633] text-white rounded font-medium hover:bg-[#f85149] disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
title={canRequestChanges ? 'Request changes' : 'Request changes requires an overall comment'}
>
Request changes
Expand All @@ -65,7 +65,7 @@
<button
onclick={() => onSubmitReview && onSubmitReview('COMMENT')}
disabled={!canSubmit || !canComment}
class="px-3 py-2 text-sm bg-[#1f6feb] text-white rounded font-medium hover:bg-[#388bfd] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
class="flex-1 px-3 py-2 text-sm bg-[#1f6feb] text-white rounded font-medium hover:bg-[#388bfd] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title={canComment ? 'Comment' : 'Add an overall comment or inline comments to submit'}
>
Comment
Expand All @@ -74,7 +74,7 @@
<button
onclick={() => onSubmitReview && onSubmitReview('APPROVE')}
disabled={!canSubmit}
class="px-3 py-2 text-sm bg-[#2ea043] text-white rounded font-medium hover:bg-[#3fb950] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
class="flex-1 px-3 py-2 text-sm bg-[#2ea043] text-white rounded font-medium hover:bg-[#3fb950] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Approve"
>
Approve
Expand Down
73 changes: 73 additions & 0 deletions src/features/pr-review/stores/pr-navigation.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { derived, type Readable } from 'svelte/store';
import { allPullRequests, pullRequestConfigs, getRepoKey } from '$shared/stores/repository-service';
import type { PullRequest } from '$integrations/github';

export interface PRNavigationEntry {
owner: string;
repo: string;
number: number;
title: string;
}

/**
* Derives a flat, ordered list of all PRs from the dashboard,
* maintaining the same order as displayed (configs order → PRs order within each repo).
*/
export const prNavigationList: Readable<PRNavigationEntry[]> = derived(
[pullRequestConfigs, allPullRequests],
([$configs, $allPRs]) => {
const entries: PRNavigationEntry[] = [];
const configs = Array.isArray($configs) ? $configs : [];

for (const config of configs) {
const repoKey = getRepoKey(config);
const prs: PullRequest[] = $allPRs[repoKey] || [];
for (const pr of prs) {
entries.push({
owner: config.org,
repo: config.repo,
number: pr.number,
title: pr.title,
});
}
}

return entries;
}
);

/**
* Given the current PR identifiers, returns the previous and next PR navigation entries.
* Wraps around at the boundaries.
*/
export function getAdjacentPRs(
list: PRNavigationEntry[],
owner: string,
repo: string,
prNumber: number
): { prev: PRNavigationEntry | null; next: PRNavigationEntry | null } {
if (list.length === 0) {
return { prev: null, next: null };
}

const currentIndex = list.findIndex(
(entry) => entry.owner === owner && entry.repo === repo && entry.number === prNumber
);

if (currentIndex === -1) {
return { prev: null, next: null };
}

// If there's only one PR, no navigation needed
if (list.length === 1) {
return { prev: null, next: null };
}

const prevIndex = (currentIndex - 1 + list.length) % list.length;
const nextIndex = (currentIndex + 1) % list.length;

return {
prev: list[prevIndex],
next: list[nextIndex],
};
}
76 changes: 76 additions & 0 deletions src/shared/navigation/Breadcrumb.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { prNavigationList, getAdjacentPRs, type PRNavigationEntry } from '$features/pr-review/stores/pr-navigation.store';
import { breadcrumbService, type BreadcrumbItem } from '../navigation/breadcrumb.service';

// Generate breadcrumbs reactively based on current page
Expand All @@ -11,10 +13,24 @@

// Only show breadcrumbs if we're not on the home page
let shouldShowBreadcrumbs = $derived(page.url.pathname !== '/');

// PR navigation: detect if we're on a PR page and compute adjacent PRs
const prMatch = $derived(page.url.pathname.match(/^\/pr\/([^/]+)\/([^/]+)\/(\d+)$/));
const adjacentPRs = $derived.by(() => {
if (!prMatch) return { prev: null, next: null };
const [, owner, repo, number] = prMatch;
return getAdjacentPRs($prNavigationList, owner, repo, parseInt(number));
});
const showPRNav = $derived(prMatch && (adjacentPRs.prev || adjacentPRs.next));

function navigateToPR(entry: PRNavigationEntry) {
goto(`/pr/${entry.owner}/${entry.repo}/${entry.number}`);
}
</script>

{#if shouldShowBreadcrumbs}
<nav class="breadcrumb-nav bg-gray-800 border-b border-gray-700 px-4 py-2" aria-label="Breadcrumb">
<div class="flex items-center justify-between">
<ol class="flex items-center space-x-2 text-sm">
{#each breadcrumbs as item, index}
<li class="flex items-center">
Expand Down Expand Up @@ -54,6 +70,36 @@
</li>
{/each}
</ol>

{#if showPRNav}
<div class="flex items-center space-x-2 ml-4 flex-shrink-0">
<button
onclick={() => adjacentPRs.prev && navigateToPR(adjacentPRs.prev)}
disabled={!adjacentPRs.prev}
class="pr-nav-btn"
title={adjacentPRs.prev ? `Previous: ${adjacentPRs.prev.title} (#${adjacentPRs.prev.number})` : 'No previous PR'}
aria-label="Previous pull request"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<span>Prev</span>
</button>
<button
onclick={() => adjacentPRs.next && navigateToPR(adjacentPRs.next)}
disabled={!adjacentPRs.next}
class="pr-nav-btn"
title={adjacentPRs.next ? `Next: ${adjacentPRs.next.title} (#${adjacentPRs.next.number})` : 'No next PR'}
aria-label="Next pull request"
>
<span>Next</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
{/if}
</div>
</nav>
{/if}

Expand Down Expand Up @@ -82,4 +128,34 @@
.breadcrumb-current {
color: #d1d5db;
}

.pr-nav-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
border: 1px solid #4b5563;
color: #d1d5db;
font-size: 0.8125rem;
font-weight: 500;
transition: all 0.2s ease;
background-color: rgba(55, 65, 81, 0.5);
}

.pr-nav-btn:hover:not(:disabled) {
background-color: rgba(59, 130, 246, 0.15);
border-color: #60a5fa;
color: #60a5fa;
}

.pr-nav-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}

.pr-nav-btn:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
</style>
Loading