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
9 changes: 7 additions & 2 deletions apps/dashboard/src/components/issues/issue-row.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommentIcon, IssuesIcon } from "@diffkit/icons";
import { cn } from "@diffkit/ui/lib/utils";
import { Link } from "@tanstack/react-router";
import { memo } from "react";
import { formatRelativeTime } from "#/lib/format-relative-time";
import type { IssueSummary } from "#/lib/github.types";

Expand All @@ -14,7 +15,11 @@ function getIssueStateProps(issue: IssueSummary) {
return { color: "text-green-500" };
}

export function IssueRow({ issue }: { issue: IssueSummary }) {
export const IssueRow = memo(function IssueRow({
issue,
}: {
issue: IssueSummary;
}) {
const { color } = getIssueStateProps(issue);
const href = `/${issue.repository.owner}/${issue.repository.name}/issues/${issue.number}`;

Expand Down Expand Up @@ -55,4 +60,4 @@ export function IssueRow({ issue }: { issue: IssueSummary }) {
)}
</Link>
);
}
});
20 changes: 16 additions & 4 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { getRouteApi, Outlet } from "@tanstack/react-router";
import { CommandPalette } from "#/components/navigation/command-palette";
import { lazy, Suspense } from "react";
import {
githubMyIssuesQueryOptions,
githubMyPullsQueryOptions,
Expand All @@ -9,7 +9,17 @@ import { useGitHubRevalidation } from "#/lib/use-github-revalidation";
import { useHasMounted } from "#/lib/use-has-mounted";
import { DashboardBottomBar } from "./dashboard-bottombar";
import { DashboardTopbar } from "./dashboard-topbar";
import { GitHubAccessDialog } from "./github-access-dialog";

const CommandPalette = lazy(() =>
import("#/components/navigation/command-palette").then((mod) => ({
default: mod.CommandPalette,
})),
);
const GitHubAccessDialog = lazy(() =>
import("./github-access-dialog").then((mod) => ({
default: mod.GitHubAccessDialog,
})),
);

const routeApi = getRouteApi("/_protected");

Expand Down Expand Up @@ -60,8 +70,10 @@ export function DashboardLayout() {
</div>
</div>
<DashboardBottomBar />
<CommandPalette />
<GitHubAccessDialog userId={user.id} />
<Suspense>
<CommandPalette />
<GitHubAccessDialog userId={user.id} />
</Suspense>
</div>
);
}
102 changes: 58 additions & 44 deletions apps/dashboard/src/components/layouts/dashboard-topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@diffkit/ui/components/dropdown-menu";
import { Link, useRouter } from "@tanstack/react-router";
import { Link, useRouter, useRouterState } from "@tanstack/react-router";
import { useTheme } from "next-themes";
import { useCallback, useEffect, useRef, useState } from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { signOutToLogin } from "#/lib/auth-actions";
import { preloadRouteOnce } from "#/lib/route-preload";
import { useGlobalShortcuts } from "#/lib/shortcuts";
Expand Down Expand Up @@ -73,7 +73,12 @@ export function DashboardTopbar({
const { theme, setTheme } = useTheme();
const [avatarLoadFailed, setAvatarLoadFailed] = useState(false);
const openTabs = useTabs();
// Store router in a ref — only used imperatively (navigate, preload),
// never read during render, so we avoid subscribing to state changes.
const router = useRouter();
const routerRef = useRef(router);
routerRef.current = router;
const pathname = useRouterState({ select: (s) => s.location.pathname });
const scrollRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
Expand Down Expand Up @@ -108,49 +113,63 @@ export function DashboardTopbar({
.slice(0, 2)
.toUpperCase();

const navItems: NavItem[] = [
{ to: "/", label: "Overview", icon: HomeIcon },
{
to: "/pulls",
label: "Pull Requests",
icon: GitPullRequestIcon,
count: counts.pulls,
},
{
to: "/issues",
label: "Issues",
icon: IssuesIcon,
count: counts.issues,
},
{
to: "/reviews",
label: "Reviews",
icon: ReviewsIcon,
count: counts.reviews,
},
];
const navItems = useMemo<NavItem[]>(
() => [
{ to: "/", label: "Overview", icon: HomeIcon },
{
to: "/pulls",
label: "Pull Requests",
icon: GitPullRequestIcon,
count: counts.pulls,
},
{
to: "/issues",
label: "Issues",
icon: IssuesIcon,
count: counts.issues,
},
{
to: "/reviews",
label: "Reviews",
icon: ReviewsIcon,
count: counts.reviews,
},
],
[counts.pulls, counts.issues, counts.reviews],
);

useEffect(() => {
if (!tabsReady) return;

void Promise.allSettled(
primaryNavRoutes.map((to) => router.preloadRoute({ to })),
primaryNavRoutes.map((to) => routerRef.current.preloadRoute({ to })),
);
}, [router, tabsReady]);
}, [tabsReady]);

useEffect(() => {
if (!tabsReady || openTabs.length === 0) return;

void Promise.allSettled(
openTabs.map((tab) => preloadRouteOnce(router, tab.url)),
openTabs.map((tab) => preloadRouteOnce(routerRef.current, tab.url)),
);
}, [router, tabsReady, openTabs]);
}, [tabsReady, openTabs]);

function navigateToTab(tab: Tab | undefined) {
if (!tab) return;
void router.navigate({ to: tab.url });
void routerRef.current.navigate({ to: tab.url });
}

const handleCloseTab = useCallback(
(id: string, tabUrl: string) => {
const isActive = pathname === tabUrl;
removeTab(id);
if (isActive) {
void routerRef.current.navigate({ to: "/" });
}
},
[pathname],
);

useGlobalShortcuts([
...Array.from(
{ length: Math.min(openTabs.length, MAX_TAB_SHORTCUTS) },
Expand All @@ -167,7 +186,7 @@ export function DashboardTopbar({
enabled: tabsReady && openTabs.length > 1,
onTrigger: () => {
const currentIndex = openTabs.findIndex(
(tab) => tab.url === router.state.location.pathname,
(tab) => tab.url === routerRef.current.state.location.pathname,
);
const nextIndex =
currentIndex === -1
Expand All @@ -181,7 +200,7 @@ export function DashboardTopbar({
enabled: tabsReady && openTabs.length > 1,
onTrigger: () => {
const currentIndex = openTabs.findIndex(
(tab) => tab.url === router.state.location.pathname,
(tab) => tab.url === routerRef.current.state.location.pathname,
);
const nextIndex =
currentIndex === -1 ? 0 : (currentIndex + 1) % openTabs.length;
Expand Down Expand Up @@ -331,14 +350,8 @@ export function DashboardTopbar({
key={tab.id}
tab={tab}
icon={Icon}
onClose={(id) => {
const isActive =
router.state.location.pathname === tab.url;
removeTab(id);
if (isActive) {
void router.navigate({ to: "/" });
}
}}
onClose={handleCloseTab}
routerRef={routerRef}
/>
);
})}
Expand All @@ -361,18 +374,19 @@ export function DashboardTopbar({
);
}

function DetailTab({
const DetailTab = memo(function DetailTab({
tab,
icon: Icon,
onClose,
routerRef,
}: {
tab: Tab;
icon: typeof GitPullRequestIcon;
onClose: (id: string) => void;
onClose: (id: string, tabUrl: string) => void;
routerRef: React.RefObject<ReturnType<typeof useRouter>>;
}) {
const router = useRouter();
const preloadTab = () => {
void preloadRouteOnce(router, tab.url);
void preloadRouteOnce(routerRef.current, tab.url);
};

return (
Expand Down Expand Up @@ -407,7 +421,7 @@ function DetailTab({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClose(tab.id);
onClose(tab.id, tab.url);
}}
className="absolute inset-y-0 right-0 flex items-center rounded-r-md bg-surface-1 pl-1.5 pr-1.5 opacity-0 transition-opacity group-hover:opacity-100"
aria-label={`Close ${tab.title}`}
Expand All @@ -419,4 +433,4 @@ function DetailTab({
</button>
</Link>
);
}
});
60 changes: 47 additions & 13 deletions apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Markdown } from "@diffkit/ui/components/markdown";
import { Skeleton } from "@diffkit/ui/components/skeleton";
import { toast } from "@diffkit/ui/components/sonner";
import { cn } from "@diffkit/ui/lib/utils";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import {
DetailActivityHeader,
Expand All @@ -37,33 +37,38 @@ import {
requestPullReviewers,
updatePullBranch,
} from "#/lib/github.functions";
import {
type GitHubQueryScope,
githubPullStatusQueryOptions,
} from "#/lib/github.query";
import type {
PullCheckRun,
PullComment,
PullCommit,
PullDetail,
PullStatus,
} from "#/lib/github.types";
import { githubCachePolicy } from "#/lib/github-cache-policy";
import { checkPermissionWarning } from "#/lib/warning-store";

export function PullDetailActivitySection({
comments,
commits,
isFetching,
status,
pr,
owner,
repo,
pullNumber,
scope,
}: {
comments?: PullComment[];
commits?: PullCommit[];
isFetching: boolean;
status: PullStatus | null;
pr: PullDetail;
owner: string;
repo: string;
pullNumber: number;
scope: GitHubQueryScope;
}) {
return (
<div className="flex flex-col">
Expand Down Expand Up @@ -112,16 +117,12 @@ export function PullDetailActivitySection({

{!pr.isMerged && pr.state !== "closed" && (
<div className="mt-6">
{status ? (
<MergeStatusCard
status={status}
owner={owner}
repo={repo}
pullNumber={pullNumber}
/>
) : (
<MergeStatusSkeleton />
)}
<MergeStatusSection
scope={scope}
owner={owner}
repo={repo}
pullNumber={pullNumber}
/>
</div>
)}

Expand All @@ -132,6 +133,39 @@ export function PullDetailActivitySection({
);
}

// ── Merge status section — owns its own polling query ────────────────

function MergeStatusSection({
scope,
owner,
repo,
pullNumber,
}: {
scope: GitHubQueryScope;
owner: string;
repo: string;
pullNumber: number;
}) {
const statusQuery = useQuery({
...githubPullStatusQueryOptions(scope, { owner, repo, pullNumber }),
refetchOnWindowFocus: "always",
refetchInterval: githubCachePolicy.status.staleTimeMs,
});

const status = statusQuery.data ?? null;

if (!status) return <MergeStatusSkeleton />;

return (
<MergeStatusCard
status={status}
owner={owner}
repo={repo}
pullNumber={pullNumber}
/>
);
}

function MergeStatusCard({
status,
owner,
Expand Down
Loading
Loading