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
1 change: 1 addition & 0 deletions apps/webapp/app/components/navigation/DashboardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function DashboardChildMenuItem({
activeIconColor={activeIconColor}
inactiveIconColor={inactiveIconColor}
to={item.path}
matchPrefix={true}
isCollapsed={isCollapsed}
disableIconHover
yieldActiveToFavorite
Expand Down
10 changes: 9 additions & 1 deletion apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,10 @@ export function SideMenu({
data-action="tasks"
isCollapsed={isCollapsed}
yieldActiveToFavorite
nestedPaths={[
`${v3EnvironmentPath(organization, project, environment)}/tasks`,
`${v3EnvironmentPath(organization, project, environment)}/agents`,
]}
Comment on lines +948 to +951

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Sidebar fix missing release note

This user-facing webapp fix has no .server-changes/ entry. The release summary omits the sidebar highlight fix.

Learn more

The sidebar change fixes a visible behavior for webapp users. Server-only, user-facing fixes require a release note under .server-changes/, and this PR changes only webapp files. The release process includes those notes in its summary.

Example: A user upgrades and reads the release summary to see whether nested task pages retain their sidebar highlight. With no entry, the fix is absent from that summary.

Recommended fix: Add a .server-changes/ Markdown file with area: webapp, type: fix, and one user-facing sentence about retaining the sidebar highlight on detail pages.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

/>
<SideMenuItem
name="Runs"
Expand All @@ -955,6 +959,7 @@ export function SideMenu({
data-action="runs"
isCollapsed={isCollapsed}
yieldActiveToFavorite
matchPrefix
/>
<SideMenuItem
name="Sessions"
Expand All @@ -965,6 +970,7 @@ export function SideMenu({
data-action="sessions"
isCollapsed={isCollapsed}
yieldActiveToFavorite
matchPrefix
/>
{(isManagedCloud ||
user.admin ||
Expand All @@ -980,6 +986,7 @@ export function SideMenu({
badge={<BetaBadge />}
isCollapsed={isCollapsed}
yieldActiveToFavorite
matchPrefix
/>
)}
</div>
Expand Down Expand Up @@ -1141,7 +1148,8 @@ function CustomizableSideMenuSection({
activeIconColor={item.activeIconColor}
inactiveIconColor={item.inactiveIconColor ?? "text-text-dimmed"}
to={item.to}
matchPrefix={item.matchPrefix}
matchPrefix={item.matchPrefix ?? (item.id !== "dashboards")}
nestedPaths={item.nestedPaths}
data-action={item.dataAction}
badge={item.badge}
isCollapsed={isCollapsed}
Expand Down
103 changes: 99 additions & 4 deletions apps/webapp/app/components/navigation/SideMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,93 @@ export function SideMenuLabel({
);
}

/**
* Normalizes a URL path by stripping any hash, query parameters, and trailing slashes.
*/
export function normalizePath(path: string): string {
const [clean = ""] = path.split("#")[0].split("?");
if (clean.length > 1 && clean.endsWith("/")) {
return clean.slice(0, -1);
}
return clean;
}

/**
* Checks whether currentPath matches targetPath, optionally checking nested paths
* while respecting segment boundaries.
*/
export function matchesPath(currentPath: string, targetPath: string, matchPrefix = false): boolean {
const normalizedCurrent = normalizePath(currentPath);
const normalizedTarget = normalizePath(targetPath);

if (normalizedCurrent === normalizedTarget) {
return true;
}

if (matchPrefix) {
return normalizedCurrent.startsWith(`${normalizedTarget}/`);
}

return false;
}

export type IsSideMenuItemActiveOptions = {
currentPath: string;
targetPath: string;
matchPrefix?: boolean;
nestedPaths?: string[];
isActiveOverride?: boolean;
yieldActiveToFavorite?: boolean;
activeFavoriteId?: string;
};

/**
* Determines whether a sidebar navigation item should be marked active.
*
* Respects:
* - Direct isActive overrides (e.g. from favorites).
* - Yielding active highlight to a matching active favorite.
* - Exact path matching.
* - Segment-boundary-aware prefix matching (e.g. /concurrency does NOT match /concurrency-limits).
* - Nested paths for parent items whose subpages live under distinct paths (e.g. Tasks).
*/
export function isSideMenuItemActive({
currentPath,
targetPath,
matchPrefix = false,
nestedPaths,
isActiveOverride,
yieldActiveToFavorite = false,
activeFavoriteId,
}: IsSideMenuItemActiveOptions): boolean {
if (isActiveOverride !== undefined) {
return isActiveOverride;
}

if (yieldActiveToFavorite && activeFavoriteId !== undefined) {
return false;
}

if (matchesPath(currentPath, targetPath, matchPrefix)) {
return true;
}

if (nestedPaths && nestedPaths.length > 0) {
const normalizedTarget = normalizePath(targetPath);
for (const nested of nestedPaths) {
const resolved =
nested.startsWith("/") && !nested.startsWith(normalizedTarget)
? `${normalizedTarget}${nested}`
: nested;
if (matchesPath(currentPath, resolved, true)) {
return true;
}
}
}

return false;
}

export function SideMenuItem({
icon,
activeIconColor,
Expand All @@ -80,6 +167,7 @@ export function SideMenuItem({
isActive: isActiveOverride,
yieldActiveToFavorite = false,
matchPrefix = false,
nestedPaths,
"data-action": dataAction,
}: {
icon?: RenderIcon;
Expand All @@ -101,6 +189,8 @@ export function SideMenuItem({
/** Overrides the default pathname === to active check (e.g. favorites match on full URL). */
isActive?: boolean;
matchPrefix?: boolean;
/** Additional subpaths (e.g. /tasks, /agents) that should activate this parent item. */
nestedPaths?: string[];
/**
* In menus that render the Favorites section (the main project menu), an active favorite owns
* the highlight, so the plain item yields its active state to it. Menus without a favorites
Expand All @@ -114,10 +204,15 @@ export function SideMenuItem({
// Only the user's OWN favorites own a view (via the marker param); markers from shared links
// don't count (see useActiveFavoriteId).
const activeFavoriteId = useActiveFavoriteId();
const isActive =
isActiveOverride ??
((pathName === to || (matchPrefix && pathName.startsWith(`${to}/`))) &&
(!yieldActiveToFavorite || activeFavoriteId === undefined));
const isActive = isSideMenuItemActive({
currentPath: pathName,
targetPath: to,
matchPrefix,
nestedPaths,
isActiveOverride,
yieldActiveToFavorite,
activeFavoriteId,
});

const isIndented = indented && !isCollapsed;

Expand Down
1 change: 1 addition & 0 deletions apps/webapp/app/components/navigation/sideMenuSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type SideMenuItemConfig = {
inactiveIconColor?: string;
to: string;
matchPrefix?: boolean;
nestedPaths?: string[];
dataAction?: string;
badge?: ReactNode;
trailingIconClassName?: string;
Expand Down
Loading