mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site/src/pages/AgentsPage): preserve ?archived in sibling navigation (#24777)
Follow-up to #24742. Navigation paths were dropping `?archived` search params, silently resetting the filter now that it's URL-derived. Fixed all sibling navigation that used bare `/agents` paths or stored only `location.pathname` without `location.search`: - **ChatTopBar** mobile back button (`md:hidden`) - **ChatTopBar** parent chat breadcrumb - **AgentsSidebar** settings gear link (stored `state.from` without search) - **AgentPageHeader** mobile menu settings link (same `state.from` bug) - **AgentAnalyticsPage** mobile back button (widened `mobileBack.to` type to accept `To`) - **SubagentTool** "View agent" external link - **AgentsPage** `navigateAfterArchive` and `handleNewAgent` navigate calls Two Storybook play stories cover the ChatTopBar back button and the sidebar settings round-trip. _Generated by Coder Agents._
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import { type FC, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useLocation } from "react-router";
|
||||
import { chatCostSummary } from "#/api/queries/chats";
|
||||
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
|
||||
import { useAuthContext } from "#/contexts/auth/AuthProvider";
|
||||
@@ -24,6 +25,7 @@ interface AgentAnalyticsPageProps {
|
||||
|
||||
const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
|
||||
const { user } = useAuthContext();
|
||||
const location = useLocation();
|
||||
const [anchor] = useState<Dayjs>(() => dayjs());
|
||||
const dateRange = createDateRange(now ?? anchor);
|
||||
|
||||
@@ -37,7 +39,12 @@ const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
|
||||
|
||||
return (
|
||||
<ScrollArea className="min-h-0 flex-1" viewportClassName="[&>div]:!block">
|
||||
<AgentPageHeader mobileBack={{ to: "/agents", label: "Agents" }} />
|
||||
<AgentPageHeader
|
||||
mobileBack={{
|
||||
to: { pathname: "/agents", search: location.search },
|
||||
label: "Agents",
|
||||
}}
|
||||
/>
|
||||
<AgentAnalyticsPageView
|
||||
summary={summaryQuery.data}
|
||||
isLoading={summaryQuery.isLoading}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useLocation, useNavigate, useParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { API, watchChats } from "#/api/api";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
@@ -63,6 +63,7 @@ const AgentsPage: FC = () => {
|
||||
useAgentsPWA();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { agentId } = useParams();
|
||||
const { permissions } = useAuthenticated();
|
||||
const { appearance } = useDashboard();
|
||||
@@ -321,7 +322,7 @@ const AgentsPage: FC = () => {
|
||||
: undefined,
|
||||
)
|
||||
) {
|
||||
navigate("/agents");
|
||||
navigate({ pathname: "/agents", search: location.search });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -454,7 +455,7 @@ const AgentsPage: FC = () => {
|
||||
if (!agentId) {
|
||||
localStorage.removeItem(emptyInputStorageKey);
|
||||
}
|
||||
navigate("/agents");
|
||||
navigate({ pathname: "/agents", search: location.search });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,7 +11,13 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, NavLink, useLocation, useOutletContext } from "react-router";
|
||||
import {
|
||||
Link,
|
||||
NavLink,
|
||||
type To,
|
||||
useLocation,
|
||||
useOutletContext,
|
||||
} from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
@@ -33,7 +39,7 @@ interface AgentPageHeaderProps {
|
||||
children?: ReactNode;
|
||||
/** When set, shows a back link on mobile instead of the logo
|
||||
* and hides the settings/analytics nav buttons. */
|
||||
mobileBack?: { to: string; label: string };
|
||||
mobileBack?: { to: To; label: string };
|
||||
chimeEnabled?: boolean;
|
||||
onToggleChime?: () => void;
|
||||
webPush?: ReturnType<typeof useWebpushNotifications>;
|
||||
@@ -167,13 +173,18 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
|
||||
className="mobile-full-width-dropdown mobile-full-width-dropdown-top [&_[role=menuitem]]:text-sm"
|
||||
>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/agents/settings" state={{ from: location.pathname }}>
|
||||
<Link
|
||||
to="/agents/settings"
|
||||
state={{ from: location.pathname + location.search }}
|
||||
>
|
||||
<SettingsIcon className="size-icon-sm" />
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/agents/analytics">
|
||||
<Link
|
||||
to={{ pathname: "/agents/analytics", search: location.search }}
|
||||
>
|
||||
<BarChart3Icon className="size-icon-sm" />
|
||||
Analytics
|
||||
</Link>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { Response } from "../Response";
|
||||
@@ -179,6 +179,7 @@ export const SubagentTool: React.FC<{
|
||||
recordingFileId,
|
||||
thumbnailFileId,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { desktopChatId, onOpenDesktop } = useDesktopPanel();
|
||||
const hasPrompt = Boolean(prompt?.trim());
|
||||
@@ -218,7 +219,7 @@ export const SubagentTool: React.FC<{
|
||||
)}
|
||||
{chatId && (
|
||||
<Link
|
||||
to={`/agents/${chatId}`}
|
||||
to={{ pathname: `/agents/${chatId}`, search: location.search }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="ml-1 inline-flex align-middle text-content-secondary opacity-50 transition-opacity hover:opacity-100"
|
||||
aria-label="View agent"
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useLocation } from "react-router";
|
||||
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
||||
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||
import { ChatTopBar } from "./ChatTopBar";
|
||||
|
||||
// Probe element rendered at /agents to verify search params are preserved
|
||||
// when the mobile back button navigates away from a chat.
|
||||
const AgentsSearchProbe = () => {
|
||||
const location = useLocation();
|
||||
return <div data-testid="agents-search">{location.search}</div>;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
chatTitle: "Build authentication feature",
|
||||
panel: {
|
||||
@@ -243,6 +252,33 @@ export const GenerateTitle: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const PreservesArchivedFilterOnMobileBack: Story = {
|
||||
decorators: mobileDecorator,
|
||||
parameters: {
|
||||
chromatic: { viewports: [390] },
|
||||
reactRouter: reactRouterParameters({
|
||||
location: {
|
||||
path: "/agents/chat-123",
|
||||
searchParams: { archived: "archived" },
|
||||
},
|
||||
routing: [
|
||||
{ path: "/agents/:agentId", useStoryElement: true },
|
||||
{ path: "/agents", element: <AgentsSearchProbe /> },
|
||||
],
|
||||
}),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const backLink = await canvas.findByLabelText("Back");
|
||||
await userEvent.click(backLink);
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId("agents-search")).toHaveTextContent(
|
||||
"?archived=archived",
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const ArchivedWithUnarchive: Story = {
|
||||
args: {
|
||||
isArchived: true,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
WandSparklesIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import type { ChatDiffStatus } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
@@ -67,6 +67,7 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
|
||||
diffStatusData,
|
||||
}) => {
|
||||
const { isEmbedded } = useEmbedContext();
|
||||
const location = useLocation();
|
||||
|
||||
const prUrl = diffStatusData?.url;
|
||||
const prState = diffStatusData?.pull_request_state;
|
||||
@@ -87,7 +88,10 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
|
||||
size="icon"
|
||||
className="inline-flex h-7 w-7 min-w-0 shrink-0 md:hidden"
|
||||
>
|
||||
<Link to="/agents" aria-label="Back">
|
||||
<Link
|
||||
to={{ pathname: "/agents", search: location.search }}
|
||||
aria-label="Back"
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -121,7 +125,12 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
|
||||
variant="subtle"
|
||||
className="h-auto max-w-[16rem] rounded-sm px-1 py-0.5 text-sm text-content-secondary shadow-none hover:bg-transparent hover:text-content-primary"
|
||||
>
|
||||
<Link to={`/agents/${parentChat.id}`}>
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/agents/${parentChat.id}`,
|
||||
search: location.search,
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{parentChat.title}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,14 @@ const ChildSearchProbe = () => {
|
||||
return <div data-testid="child-search">{location.search}</div>;
|
||||
};
|
||||
|
||||
// Probe element used by the settings-link preservation story to surface the
|
||||
// state.from value passed when navigating to settings.
|
||||
const SettingsStateProbe = () => {
|
||||
const location = useLocation();
|
||||
const from = (location.state as { from?: string })?.from ?? "";
|
||||
return <div data-testid="settings-state-from">{from}</div>;
|
||||
};
|
||||
|
||||
const defaultModelOptions: ModelSelectorOption[] = [
|
||||
{
|
||||
id: "openai:gpt-4o",
|
||||
@@ -1594,3 +1602,43 @@ export const SettingsAPIKeysNonAdmin: Story = {
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const PreservesArchivedFilterOnSettingsNavigation: Story = {
|
||||
args: {
|
||||
chats: [
|
||||
buildChat({
|
||||
id: "archived-settings-1",
|
||||
title: "Archived settings target",
|
||||
archived: true,
|
||||
updated_at: recentTimestamp,
|
||||
}),
|
||||
],
|
||||
archivedFilter: "archived",
|
||||
},
|
||||
parameters: {
|
||||
reactRouter: reactRouterParameters({
|
||||
location: {
|
||||
path: "/agents",
|
||||
searchParams: { archived: "archived" },
|
||||
},
|
||||
routing: [
|
||||
{
|
||||
path: "/agents/settings",
|
||||
element: <SettingsStateProbe />,
|
||||
},
|
||||
...agentsRouting,
|
||||
],
|
||||
}),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const settingsLink = await canvas.findByLabelText("Settings");
|
||||
await userEvent.click(settingsLink);
|
||||
await waitFor(() => {
|
||||
const fromValue =
|
||||
canvas.getByTestId("settings-state-from").textContent ?? "";
|
||||
expect(fromValue).toContain("/agents");
|
||||
expect(fromValue).toContain("archived=archived");
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1105,7 +1105,10 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
isSettingsPanel && "text-content-primary",
|
||||
)}
|
||||
>
|
||||
<Link to="/agents/settings" state={{ from: location.pathname }}>
|
||||
<Link
|
||||
to="/agents/settings"
|
||||
state={{ from: location.pathname + location.search }}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -1126,7 +1129,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
icon={SquarePenIcon}
|
||||
label="New Agent"
|
||||
active={!activeChatId && sidebarView.panel === "chats"}
|
||||
to="/agents"
|
||||
to={`/agents${location.search}`}
|
||||
onClick={onBeforeNewAgent}
|
||||
disabled={isCreating}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user