diff --git a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx index 8c86360..f9060c3 100644 --- a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx @@ -18,9 +18,12 @@ import { Link, type useRouter, useRouterState } from "@tanstack/react-router"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { preloadRouteOnce } from "#/lib/route-preload"; import { + isMergedTab, + removeMergedTabs, removeOtherTabs, removeTab, removeTabsToRight, + reorderTabs, type Tab, useTabs, } from "#/lib/tab-store"; @@ -91,6 +94,7 @@ interface DashboardTabsProps { export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { const openTabs = useTabs(); const pathname = useRouterState({ select: (s) => s.location.pathname }); + const dragTabRef = useRef(null); const [contextTab, setContextTab] = useState<{ tab: Tab; index: number; @@ -103,6 +107,29 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { handleWheel, } = useScrollShadows(openTabs.length); + const handleDragStart = useCallback((id: string) => { + dragTabRef.current = id; + }, []); + + const handleDragOver = useCallback( + (targetId: string) => { + const dragId = dragTabRef.current; + if (!dragId || dragId === targetId) return; + const fromIndex = openTabs.findIndex((t) => t.id === dragId); + const toIndex = openTabs.findIndex((t) => t.id === targetId); + if (fromIndex === -1 || toIndex === -1) return; + const next = [...openTabs]; + const [moved] = next.splice(fromIndex, 1); + next.splice(toIndex, 0, moved); + reorderTabs(next); + }, + [openTabs], + ); + + const handleDragEnd = useCallback(() => { + dragTabRef.current = null; + }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: pathname is intentionally used as a trigger to re-run when the route changes useEffect(() => { const container = scrollRef.current; @@ -151,6 +178,21 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { removeTabsToRight(contextTab.tab.id); }, [contextTab]); + const handleContextCloseMerged = useCallback(() => { + const activeTabWillClose = openTabs.find( + (t) => pathname === t.url && isMergedTab(t), + ); + removeMergedTabs(); + if (activeTabWillClose) { + const remaining = openTabs.filter((t) => !isMergedTab(t)); + void routerRef.current.navigate({ + to: remaining[0]?.url ?? "/", + }); + } + }, [openTabs, pathname, routerRef]); + + const hasMergedTabs = openTabs.some(isMergedTab); + if (openTabs.length === 0) return null; return ( @@ -195,6 +237,9 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { tab={tab} icon={Icon} onClose={handleCloseTab} + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDragEnd={handleDragEnd} onContextMenu={() => { setContextTab({ tab, index }); }} @@ -224,6 +269,17 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { Close tabs to the right + + + Close merged + @@ -234,12 +290,18 @@ const DetailTab = memo(function DetailTab({ tab, icon: Icon, onClose, + onDragStart, + onDragOver, + onDragEnd, onContextMenu, routerRef, }: { tab: Tab; icon: typeof GitPullRequestIcon; onClose: (id: string, tabUrl: string) => void; + onDragStart: (id: string) => void; + onDragOver: (id: string) => void; + onDragEnd: () => void; onContextMenu: () => void; routerRef: React.RefObject>; }) { @@ -250,6 +312,17 @@ const DetailTab = memo(function DetailTab({ return ( { + e.dataTransfer.effectAllowed = "move"; + onDragStart(tab.id); + }} + onDragOver={(e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + onDragOver(tab.id); + }} + onDragEnd={onDragEnd} preload={false} onMouseEnter={preloadTab} onFocus={preloadTab} diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx index 1bb9876..17b7c88 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx @@ -126,6 +126,7 @@ export function PullDetailContent({ url: `/${owner}/${repo}/pull/${pullNumber}`, repo: `${owner}/${repo}`, iconColor: getPrStateConfig(pr).color, + merged: pr.isMerged, } : null, ); diff --git a/apps/dashboard/src/components/pulls/review/review-page.tsx b/apps/dashboard/src/components/pulls/review/review-page.tsx index e7b0e2e..720349a 100644 --- a/apps/dashboard/src/components/pulls/review/review-page.tsx +++ b/apps/dashboard/src/components/pulls/review/review-page.tsx @@ -276,6 +276,7 @@ export function ReviewPage() { iconColor: getPrStateConfig(pr).color, additions: pr.additions, deletions: pr.deletions, + merged: pr.isMerged, } : null, ); diff --git a/apps/dashboard/src/lib/tab-store.ts b/apps/dashboard/src/lib/tab-store.ts index 4d6e9b9..ba5fe8e 100644 --- a/apps/dashboard/src/lib/tab-store.ts +++ b/apps/dashboard/src/lib/tab-store.ts @@ -13,6 +13,7 @@ export interface Tab { avatarUrl?: string; additions?: number; deletions?: number; + merged?: boolean; } export const TABS_STORAGE_KEY = "diffkit:tabs"; @@ -88,7 +89,8 @@ export function addTab(tab: Tab) { existing.title === tab.title && existing.iconColor === tab.iconColor && existing.additions === tab.additions && - existing.deletions === tab.deletions + existing.deletions === tab.deletions && + existing.merged === tab.merged ) return; tabs = tabs.map((t) => (t.id === tab.id ? tab : t)); @@ -116,6 +118,26 @@ export function removeTabsToRight(id: string) { emitChange(); } +export function isMergedTab(t: Tab): boolean { + return ( + t.merged === true || + ((t.type === "pull" || t.type === "review") && + t.iconColor === "text-purple-500") + ); +} + +export function removeMergedTabs() { + const next = tabs.filter((t) => !isMergedTab(t)); + if (next.length === tabs.length) return; + tabs = next; + emitChange(); +} + +export function reorderTabs(newOrder: Tab[]) { + tabs = newOrder; + emitChange(); +} + const emptyTabs: Tab[] = []; export function useTabs() { diff --git a/apps/dashboard/src/lib/use-register-tab.ts b/apps/dashboard/src/lib/use-register-tab.ts index 6961b53..fd03974 100644 --- a/apps/dashboard/src/lib/use-register-tab.ts +++ b/apps/dashboard/src/lib/use-register-tab.ts @@ -12,6 +12,7 @@ export function useRegisterTab( avatarUrl?: string; additions?: number; deletions?: number; + merged?: boolean; } | null, ) { useEffect(() => { @@ -31,6 +32,7 @@ export function useRegisterTab( avatarUrl: tab.avatarUrl, additions: tab.additions, deletions: tab.deletions, + merged: tab.merged, }); }, [ tab?.type, @@ -42,5 +44,6 @@ export function useRegisterTab( tab?.avatarUrl, tab?.additions, tab?.deletions, + tab?.merged, ]); }