Skip to content
Closed
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
6 changes: 5 additions & 1 deletion apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
usePanelNavigationSuppression,
} from "../panelAnimations";
import { BackgroundThreadAnnouncer } from "./BackgroundThreadAnnouncer";
import { NavigationHistoryControls } from "./NavigationHistoryControls";
import LegacyThreadSidebar from "./LegacySidebar";
import { useThreadVisitedMigration } from "../hooks/useThreadVisitedMigration";
import ThreadSidebar from "./Sidebar";
Expand Down Expand Up @@ -204,6 +205,7 @@ function SidebarControl() {
Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""}
</TooltipPopup>
</Tooltip>
<NavigationHistoryControls onBackdrop={isSidebarVisible && stageBackdropVariant !== null} />
</div>
);
}
Expand Down Expand Up @@ -372,6 +374,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
const sidebarProviderStyle = {
"--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, sidebarMinimumWidth, sidebarMaximumWidth)}px`,
"--panel-animation-duration": `${panelAnimationDurationMs}ms`,
"--workspace-titlebar-content-left":
"calc(var(--workspace-controls-left) + var(--workspace-titlebar-control-size) + var(--workspace-titlebar-control-gap) + var(--workspace-history-controls-width))",
...windowControlsInsetStyle,
} as CSSProperties;

Expand All @@ -398,7 +402,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
return (
<PanelAnimationSuppressionProvider value={panelAnimationsSuppressed}>
<SidebarProvider
className="h-dvh! min-h-0!"
className="h-dvh! min-h-0! [--workspace-history-controls-width:0px] md:[--workspace-history-controls-width:3.5rem]"
data-panel-animations={routePanelAnimationsActive ? "true" : "false"}
defaultOpen
style={sidebarProviderStyle}
Expand Down
58 changes: 58 additions & 0 deletions apps/web/src/components/NavigationHistoryControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useAtomValue } from "@effect/atom-react";
import { useRouter } from "@tanstack/react-router";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { useState, useSyncExternalStore } from "react";

import { shortcutLabelForCommand } from "../keybindings";
import { createNavigationHistoryStore } from "../navigationHistory";
import { primaryServerKeybindingsAtom } from "../state/server";
import { Button } from "./ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";

export function NavigationHistoryControls({ onBackdrop }: { onBackdrop: boolean }) {
const { history } = useRouter();
const [store] = useState(() => {
const navigation = window.navigation;
const currentEntry = navigation?.currentEntry;
const forwardEntries = currentEntry ? navigation.entries().length - currentEntry.index - 1 : 0;
return createNavigationHistoryStore(history, forwardEntries);
});
const { canGoBack, canGoForward } = useSyncExternalStore(store.subscribe, store.getSnapshot);
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const controls = [
["Back", "navigation.back", ArrowLeftIcon, canGoBack, () => history.back()],
["Forward", "navigation.forward", ArrowRightIcon, canGoForward, () => history.forward()],
] as const;

return (
<div
className="pointer-events-auto hidden items-center [-webkit-app-region:no-drag] md:flex"
role="group"
aria-label="Navigation history"
>
{controls.map(([label, command, Icon, enabled, onClick]) => {
const shortcut = shortcutLabelForCommand(keybindings, command);
return (
<Tooltip key={command}>
<TooltipTrigger render={<span className="relative flex" />}>
<Button
aria-label={label}
disabled={!enabled}
onClick={onClick}
size="icon-sm"
variant={onBackdrop ? "media-navigation" : "ghost"}
className={onBackdrop ? "relative top-auto translate-y-0" : undefined}
>
<Icon />
</Button>
</TooltipTrigger>
<TooltipPopup side="bottom">
{label}
{shortcut ? ` (${shortcut})` : ""}
</TooltipPopup>
</Tooltip>
);
})}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,10 @@ export function PullRequestDetailPanel({
: null;
const [threadPickerOpen, setThreadPickerOpen] = useState(false);
const [openReviewRequest, setOpenReviewRequest] = useState(0);
const [tab, setTab] = useState<PullRequestDetailTab>(initialTab);
const [tabSelection, setTabSelection] = useState({ initialTab, tab: initialTab });
const tab = tabSelection.initialTab === initialTab ? tabSelection.tab : initialTab;
if (tabSelection.initialTab !== initialTab) setTabSelection({ initialTab, tab: initialTab });
const setTab = (next: PullRequestDetailTab) => setTabSelection({ initialTab, tab: next });
const selectTab = (next: PullRequestDetailTab) => {
setTab(next);
onTabChange?.(next);
Expand Down
54 changes: 17 additions & 37 deletions apps/web/src/components/pullRequest/PullRequestsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {
PullRequestListState,
} from "@t3tools/contracts";
import { useAtomValue } from "@effect/atom-react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useSearch } from "@tanstack/react-router";
import { ArrowDownUpIcon, LayersIcon, Plug2Icon } from "lucide-react";
import { useEffect, useEffectEvent, useRef, useState, type ComponentProps } from "react";

Expand Down Expand Up @@ -53,6 +53,10 @@ import {
} from "./pullRequestListPreferences";
import { environmentMachineIcon } from "../EnvironmentMachineIcon";
import { PullRequestDetailPanel } from "./PullRequestDetailPanel";
import {
usePullRequestHistorySelection,
usePullRequestNavigation,
} from "./usePullRequestNavigation";
import {
PullRequestFiltersMenu,
PullRequestSearchInput,
Expand All @@ -68,7 +72,6 @@ import {
usePullRequestDisplayGroups,
usePullRequestQueryFilters,
usePullRequestScope,
type PullRequestsSearch,
} from "./usePullRequestListView";
import { pullRequestGroups, pullRequestListEntryId } from "./pullRequestListGroups.logic";
import { PullRequestListEmptyState } from "./PullRequestListEmptyState";
Expand Down Expand Up @@ -143,7 +146,6 @@ export function PullRequestsPage() {
const sort = search.sort ?? "ready";
const statsPolicy: PullRequestStatsPolicy =
sort === "ready" || sort === "largest" || sort === "smallest" ? "eager" : "visible";
const navigate = useNavigate({ from: "/pull-requests" });
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const {
capableEnvironments,
Expand Down Expand Up @@ -195,38 +197,10 @@ export function PullRequestsPage() {
(renderedPullRequestSurface?.environmentId as EnvironmentId | undefined) ??
selectedProject?.environmentId ??
null;
const updateSearch = (patch: {
[Key in keyof PullRequestsSearch]?: PullRequestsSearch[Key] | undefined;
}) =>
void navigate({
search: (previous: PullRequestsSearch): PullRequestsSearch => {
const next = { ...previous, ...patch };
return {
involvement: next.involvement ?? previous.involvement,
state: next.state ?? previous.state,
...(next.sort && next.sort !== "ready" ? { sort: next.sort } : {}),
...(next.repository ? { repository: next.repository } : {}),
...(next.number ? { number: next.number } : {}),
...(next.projectId ? { projectId: next.projectId } : {}),
...(next.environmentId ? { environmentId: next.environmentId } : {}),
...(next.host ? { host: next.host } : {}),
...(next.selectedHost ? { selectedHost: next.selectedHost } : {}),
...(next.selectedProjectId ? { selectedProjectId: next.selectedProjectId } : {}),
...(next.selectedEnvironmentId
? { selectedEnvironmentId: next.selectedEnvironmentId }
: {}),
...(next.q ? { q: next.q } : {}),
...(next.draft ? { draft: next.draft } : {}),
...(next.review ? { review: next.review } : {}),
...(next.checks ? { checks: next.checks } : {}),
...(next.author ? { author: next.author } : {}),
...(next.labels && next.labels.length > 0 ? { labels: next.labels } : {}),
};
},
replace: true,
});
const updateSearch = usePullRequestNavigation();

const clearedSelection = {
tab: undefined,
repository: undefined,
number: undefined,
selectedProjectId: undefined,
Expand Down Expand Up @@ -999,10 +973,11 @@ export function PullRequestsPage() {
if (rightPanelState.isOpen) setMaximized((value) => !value);
};
const onGuideShown = () => setMaximized(true);
useEffect(() => {
if (!pullRequestsSupported || rightPanelRef === null || linkedSelection === null) return;
useRightPanelStore.getState().openPullRequest(rightPanelRef, linkedSelection);
}, [linkedSelection, pullRequestsSupported, rightPanelRef]);
usePullRequestHistorySelection(
pullRequestsSupported ? rightPanelRef : null,
linkedSelection,
search,
);

const selected =
rightPanelState.isOpen && activePullRequestSurface !== null
Expand All @@ -1027,6 +1002,7 @@ export function PullRequestsPage() {
surface === null
? clearedSelection
: {
tab: surface.detailTab,
repository: surface.repository,
number: surface.number,
selectedProjectId: surface.projectId as ProjectId,
Expand Down Expand Up @@ -1063,6 +1039,7 @@ export function PullRequestsPage() {
if (rightPanelRef === null) return;
useRightPanelStore.getState().openPullRequest(rightPanelRef, entry);
updateSearch({
tab: undefined,
repository: entry.repository,
number: entry.number,
selectedProjectId: entry.projectId,
Expand Down Expand Up @@ -1483,6 +1460,8 @@ export function PullRequestsPage() {
pullRequestStatusSeeds={listedPullRequestTabStatuses}
>
<PullRequestDetailPanel
initialTab={search.tab ?? "summary"}
onTabChange={(tab) => updateSearch({ tab })}
getShortcutContext={getShortcutContext}
shortcutsEnabled={activePullRequestSurface?.id === renderedPullRequestSurface.id}
key={renderedPullRequestSurface.id}
Expand All @@ -1498,6 +1477,7 @@ export function PullRequestsPage() {
environmentId: panelEnvironmentId,
});
updateSearch({
tab: undefined,
repository: reference.repository,
number: reference.number,
selectedHost: reference.host,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ describe("pull requests route search", () => {
expect(matchedSearch("/pull-requests?q=fix")?.q).toBe("fix");
expect(matchedSearch("/pull-requests?q=%224089%22")?.q).toBe("4089");
});

it("restores supported PR detail tabs and drops unknown tabs", () => {
for (const tab of ["code", "timeline", "guide"]) {
expect(matchedSearch(`/pull-requests?tab=${tab}`)?.tab).toBe(tab);
}
expect(matchedSearch("/pull-requests?tab=unknown")?.tab).toBeUndefined();
});
});
2 changes: 2 additions & 0 deletions apps/web/src/components/pullRequest/usePullRequestListView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type PullRequestListState,
} from "@t3tools/contracts";
import { useState } from "react";
import type { PullRequestDetailTab } from "../../rightPanelStore";

import { useAllEnvironmentShellsBootstrapped, useProjects } from "~/state/entities";
import { useEnvironments } from "~/state/environments";
Expand Down Expand Up @@ -41,6 +42,7 @@ import { pullRequestFilterProjects } from "./pullRequestProjectFilter.logic";
import { pullRequestListEntryId } from "./pullRequestListGroups.logic";

export interface PullRequestsSearch extends PullRequestListPreferences {
readonly tab?: PullRequestDetailTab | undefined;
readonly environmentId?: EnvironmentId;
readonly projectId?: ProjectId;
readonly host?: string;
Expand Down
Loading
Loading