diff --git a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx index 2423a4e..1f1924a 100644 --- a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx @@ -1,13 +1,28 @@ import { + ChevronRightIcon, CloseIcon, GitPullRequestIcon, IssuesIcon, + Remove01Icon, ReviewsIcon, } from "@diffkit/icons"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@diffkit/ui/components/context-menu"; +import { cn } from "@diffkit/ui/lib/utils"; import { Link, type useRouter, useRouterState } from "@tanstack/react-router"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { preloadRouteOnce } from "#/lib/route-preload"; -import { removeTab, type Tab, useTabs } from "#/lib/tab-store"; +import { + removeOtherTabs, + removeTab, + removeTabsToRight, + type Tab, + useTabs, +} from "#/lib/tab-store"; const tabIconMap = { pull: GitPullRequestIcon, @@ -45,7 +60,25 @@ function useScrollShadows(tabCount: number) { return () => ro.disconnect(); }, [tabCount, updateScrollState]); - return { scrollRef, canScrollLeft, canScrollRight, updateScrollState }; + const handleWheel = useCallback( + (e: React.WheelEvent) => { + const el = scrollRef.current; + if (!el || el.scrollWidth <= el.clientWidth) return; + if (e.deltaY === 0) return; + e.preventDefault(); + el.scrollLeft += e.deltaY; + updateScrollState(); + }, + [updateScrollState], + ); + + return { + scrollRef, + canScrollLeft, + canScrollRight, + updateScrollState, + handleWheel, + }; } interface DashboardTabsProps { @@ -56,8 +89,14 @@ interface DashboardTabsProps { export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { const openTabs = useTabs(); const pathname = useRouterState({ select: (s) => s.location.pathname }); - const { scrollRef, canScrollLeft, canScrollRight, updateScrollState } = - useScrollShadows(openTabs.length); + const contextTabRef = useRef<{ tab: Tab; index: number } | null>(null); + const { + scrollRef, + canScrollLeft, + canScrollRight, + updateScrollState, + handleWheel, + } = useScrollShadows(openTabs.length); // biome-ignore lint/correctness/useExhaustiveDependencies: pathname is intentionally used as a trigger to re-run when the route changes useEffect(() => { @@ -94,46 +133,102 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { [pathname, routerRef], ); + const handleContextClose = useCallback(() => { + const ctx = contextTabRef.current; + if (!ctx) return; + handleCloseTab(ctx.tab.id, ctx.tab.url); + }, [handleCloseTab]); + + const handleContextCloseOthers = useCallback(() => { + const ctx = contextTabRef.current; + if (!ctx) return; + if (pathname !== ctx.tab.url) { + void routerRef.current.navigate({ to: ctx.tab.url }); + } + removeOtherTabs(ctx.tab.id); + }, [pathname, routerRef]); + + const handleContextCloseRight = useCallback(() => { + const ctx = contextTabRef.current; + if (!ctx) return; + removeTabsToRight(ctx.tab.id); + }, []); + if (openTabs.length === 0) return null; return (
-
-
-
- {/* biome-ignore lint/a11y/noStaticElementInteractions: scroll container needs onScroll for gradient visibility */} -
- {openTabs.map((tab) => { - const Icon = tabIconMap[tab.type]; - return ( - - ); - })} -
-
+ + +
+
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: scroll container needs onScroll for gradient visibility */} +
+ {openTabs.map((tab, index) => { + const Icon = tabIconMap[tab.type]; + return ( + { + contextTabRef.current = { tab, index }; + }} + routerRef={routerRef} + /> + ); + })} +
+
+ + + + + Close + + + + Close other tabs + + + + Close tabs to the right + + +
); } @@ -142,11 +237,13 @@ const DetailTab = memo(function DetailTab({ tab, icon: Icon, onClose, + onContextMenu, routerRef, }: { tab: Tab; icon: typeof GitPullRequestIcon; onClose: (id: string, tabUrl: string) => void; + onContextMenu: () => void; routerRef: React.RefObject>; }) { const preloadTab = () => { @@ -160,11 +257,16 @@ const DetailTab = memo(function DetailTab({ onMouseEnter={preloadTab} onFocus={preloadTab} onTouchStart={preloadTab} + onContextMenu={onContextMenu} activeOptions={{ exact: true }} activeProps={{ className: "active" }} className="group relative flex h-8 shrink-0 items-center gap-1.5 rounded-md px-3 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-surface-1 hover:text-foreground [&.active]:bg-surface-1 [&.active]:text-foreground" > - + {tab.title} {tab.type === "review" ? ( 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 fce3572..f5ce918 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 { CircleIcon, Delete01Icon, EditIcon, + GitBranchIcon, GitCommitIcon, GitMergeIcon, GitPullRequestIcon, @@ -79,6 +80,7 @@ export function PullDetailActivitySection({ repo, pullNumber, scope, + headRefDeleted, }: { comments?: PullComment[]; commits?: PullCommit[]; @@ -92,6 +94,7 @@ export function PullDetailActivitySection({ repo: string; pullNumber: number; scope: GitHubQueryScope; + headRefDeleted: boolean; }) { return (
@@ -167,7 +170,9 @@ export function PullDetailActivitySection({
)} @@ -214,20 +219,26 @@ function MergeStatusSection({ function MergedBranchBanner({ owner, repo, + pullNumber, branchName, + headRefDeleted, }: { owner: string; repo: string; + pullNumber: number; branchName: string; + headRefDeleted: boolean; }) { const [isDeleting, setIsDeleting] = useState(false); const [isDeleted, setIsDeleted] = useState(false); + const deleted = headRefDeleted || isDeleted; + const handleDelete = async () => { setIsDeleting(true); try { const result = await deleteBranch({ - data: { owner, repo, branch: branchName }, + data: { owner, repo, branch: branchName, pullNumber }, }); if (result.ok) { setIsDeleted(true); @@ -248,7 +259,7 @@ function MergedBranchBanner({

- {isDeleted ? ( + {deleted ? ( <> Branch{" "} @@ -266,7 +277,7 @@ function MergedBranchBanner({ )}

- {!isDeleted && ( + {!deleted && (