diff --git a/apps/dashboard/src/components/issues/labels-section.tsx b/apps/dashboard/src/components/issues/labels-section.tsx index 9c2b20f..e19a5a6 100644 --- a/apps/dashboard/src/components/issues/labels-section.tsx +++ b/apps/dashboard/src/components/issues/labels-section.tsx @@ -4,7 +4,7 @@ import { PopoverContent, PopoverTrigger, } from "@diffkit/ui/components/popover"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo, useRef, useState } from "react"; import { createRepoLabel, setIssueLabels } from "#/lib/github.functions"; import { @@ -52,16 +52,23 @@ export function LabelsSection({ pageQueryKey: readonly unknown[]; }) { const { mutate } = useOptimisticMutation(); + const queryClient = useQueryClient(); const [pickerOpen, setPickerOpen] = useState(false); const [search, setSearch] = useState(""); const [pending, setPending] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); const listRef = useRef(null); + const labelsOptions = githubRepoLabelsQueryOptions(scope, { owner, repo }); + const repoLabelsQuery = useQuery({ - ...githubRepoLabelsQueryOptions(scope, { owner, repo }), + ...labelsOptions, enabled: pickerOpen, }); + + const prefetchLabels = useCallback(() => { + void queryClient.prefetchQuery(labelsOptions); + }, [queryClient, labelsOptions]); const repoLabels = repoLabelsQuery.data ?? []; const activeNames = useMemo( @@ -201,6 +208,8 @@ export function LabelsSection({ + + ); +}); diff --git a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx index d50ed96..23dd79c 100644 --- a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx @@ -1,5 +1,4 @@ import { - CloseIcon, GitPullRequestIcon, HomeIcon, IssuesIcon, @@ -21,13 +20,13 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from "@diffkit/ui/components/dropdown-menu"; -import { Link, useRouter, useRouterState } from "@tanstack/react-router"; +import { Link, useRouter } from "@tanstack/react-router"; import { useTheme } from "next-themes"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { DashboardTabs } from "#/components/layouts/dashboard-tabs"; import { signOutToLogin } from "#/lib/auth-actions"; -import { preloadRouteOnce } from "#/lib/route-preload"; import { useGlobalShortcuts } from "#/lib/shortcuts"; -import { removeTab, type Tab, useTabs } from "#/lib/tab-store"; +import { type Tab, useTabs } from "#/lib/tab-store"; interface DashboardTopbarProps { user: { @@ -56,12 +55,6 @@ const themeOptions = [ { value: "system", icon: SystemIcon, label: "System" }, ] as const; -const tabIconMap = { - pull: GitPullRequestIcon, - issue: IssuesIcon, - review: ReviewsIcon, -} as const; - const primaryNavRoutes = ["/", "/pulls", "/issues", "/reviews"] as const; const MAX_TAB_SHORTCUTS = 9; @@ -78,32 +71,6 @@ export function DashboardTopbar({ const router = useRouter(); const routerRef = useRef(router); routerRef.current = router; - const pathname = useRouterState({ select: (s) => s.location.pathname }); - const scrollRef = useRef(null); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(false); - - const updateScrollState = useCallback(() => { - const el = scrollRef.current; - if (!el) return; - setCanScrollLeft(el.scrollLeft > 0); - setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1); - }, []); - - useEffect(() => { - const el = scrollRef.current; - if (!el) return; - const ro = new ResizeObserver(updateScrollState); - ro.observe(el); - return () => ro.disconnect(); - }, [updateScrollState]); - - useEffect(() => { - const el = scrollRef.current; - if (!el || openTabs.length === 0) return; - el.scrollLeft = el.scrollWidth; - updateScrollState(); - }, [openTabs.length, updateScrollState]); const displayName = user.name ?? user.email; const initials = displayName @@ -146,30 +113,11 @@ export function DashboardTopbar({ ); }, [tabsReady]); - useEffect(() => { - if (!tabsReady || openTabs.length === 0) return; - - void Promise.allSettled( - openTabs.map((tab) => preloadRouteOnce(routerRef.current, tab.url)), - ); - }, [tabsReady, openTabs]); - function navigateToTab(tab: Tab | undefined) { if (!tab) return; 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) }, @@ -319,46 +267,7 @@ export function DashboardTopbar({
- {openTabs.length > 0 && ( -
-
-
-
-
- {/* biome-ignore lint/a11y/noStaticElementInteractions: scroll container needs onScroll for gradient visibility */} -
- {openTabs.map((tab) => { - const Icon = tabIconMap[tab.type]; - return ( - - ); - })} -
-
-
- )} +
@@ -373,64 +282,3 @@ export function DashboardTopbar({ ); } - -const DetailTab = memo(function DetailTab({ - tab, - icon: Icon, - onClose, - routerRef, -}: { - tab: Tab; - icon: typeof GitPullRequestIcon; - onClose: (id: string, tabUrl: string) => void; - routerRef: React.RefObject>; -}) { - const preloadTab = () => { - void preloadRouteOnce(routerRef.current, tab.url); - }; - - return ( - - - {tab.title} - {tab.type === "review" ? ( - - {tab.additions != null && ( - +{tab.additions} - )} - {tab.deletions != null && ( - -{tab.deletions} - )} - - ) : ( - - #{tab.number} - - )} - - - ); -}); diff --git a/apps/dashboard/src/components/pulls/detail/pull-body-section.tsx b/apps/dashboard/src/components/pulls/detail/pull-body-section.tsx index 0b2e03e..d82697c 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-body-section.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-body-section.tsx @@ -297,43 +297,47 @@ export function PullBodySection({ ) : ( )} - {isSaving ? "Saving..." : "Save"} + Save
); } + const hasDropdownOptions = !!pr.body || isAuthor; + return (
- - - - - - {pr.body && ( - { - void navigator.clipboard.writeText(pr.body); - }} + {hasDropdownOptions && ( + + + + + + {pr.body && ( + { + void navigator.clipboard.writeText(pr.body); + }} + > + + Copy as Markdown + + )} + {isAuthor && ( + + + Edit + + )} + + + )} {pr.body ? ( {pr.body} ) : ( diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx index 00b3448..d4de677 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx @@ -5,6 +5,7 @@ import { GitCommitIcon, GitMergeIcon, MoreHorizontalIcon, + RefreshCwIcon, XIcon, } from "@diffkit/icons"; import { Button } from "@diffkit/ui/components/button"; @@ -23,6 +24,7 @@ import { import { Markdown } from "@diffkit/ui/components/markdown"; import { Skeleton } from "@diffkit/ui/components/skeleton"; import { toast } from "@diffkit/ui/components/sonner"; +import { Spinner } from "@diffkit/ui/components/spinner"; import { cn } from "@diffkit/ui/lib/utils"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; @@ -622,11 +624,18 @@ function UpdateBranchButton({ size="xs" disabled={isUpdating} className="rounded-r-none" + iconLeft={ + isUpdating ? ( + + ) : ( + + ) + } onClick={() => { void handleUpdate(); }} > - {isUpdating ? "Updating…" : "Update branch"} + Update branch @@ -739,9 +748,15 @@ function MergeFooter({ void handleMerge(); }} className="rounded-r-none" - iconLeft={} + iconLeft={ + isMerging ? ( + + ) : ( + + ) + } > - {isMerging ? "Merging…" : currentStrategy.label} + {currentStrategy.label} diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-sidebar.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-sidebar.tsx index ae20de9..eabc0c8 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-sidebar.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-sidebar.tsx @@ -14,7 +14,7 @@ import { PopoverContent, PopoverTrigger, } from "@diffkit/ui/components/popover"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo, useRef, useState } from "react"; import { DetailParticipantAvatars, @@ -131,20 +131,32 @@ function ReviewersSection({ scope: GitHubQueryScope; }) { const { mutate } = useOptimisticMutation(); + const queryClient = useQueryClient(); const [pickerOpen, setPickerOpen] = useState(false); const [search, setSearch] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1); const listRef = useRef(null); + const collaboratorsOptions = githubRepoCollaboratorsQueryOptions(scope, { + owner, + repo, + }); + const teamsOptions = githubOrgTeamsQueryOptions(scope, owner); + const collaboratorsQuery = useQuery({ - ...githubRepoCollaboratorsQueryOptions(scope, { owner, repo }), + ...collaboratorsOptions, enabled: pickerOpen, }); const teamsQuery = useQuery({ - ...githubOrgTeamsQueryOptions(scope, owner), + ...teamsOptions, enabled: pickerOpen, }); + const prefetchReviewers = useCallback(() => { + void queryClient.prefetchQuery(collaboratorsOptions); + void queryClient.prefetchQuery(teamsOptions); + }, [queryClient, collaboratorsOptions, teamsOptions]); + const collaborators = collaboratorsQuery.data ?? []; const teams = teamsQuery.data ?? []; const isLoading = collaboratorsQuery.isLoading || teamsQuery.isLoading; @@ -346,6 +358,8 @@ function ReviewersSection({
diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx index 6566f16..a4ff290 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx @@ -31,7 +31,7 @@ export const Route = createFileRoute("/_protected/$owner/$repo/pull/$pullId")({ head: ({ loaderData, match, params }) => { const pull = loaderData?.detail; const title = pull - ? formatPageTitle(`PR #${pull.number}: ${pull.title}`) + ? formatPageTitle(pull.title) : formatPageTitle(`PR #${params.pullId}`); return buildSeo({ diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx index d95bd50..f3ff7b8 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx @@ -53,7 +53,7 @@ export const Route = createFileRoute("/_protected/$owner/$repo/review/$pullId")( head: ({ loaderData, match, params }) => { const pull = loaderData?.pageData?.detail; const title = pull - ? formatPageTitle(`Review PR #${pull.number}: ${pull.title}`) + ? formatPageTitle(pull.title) : formatPageTitle(`Review PR #${params.pullId}`); return buildSeo({