Skip to content

Commit 8a2eb70

Browse files
committed
fix(webapp): stop favorites capturing pagination position in their URLs
Favoriting a paged view saved the cursor, direction, and page params, so the favorite linked to a soon-stale slice of results, only lit up at that exact cursor, and starring the same view from another page created a duplicate. Saved and matched favorite URLs now drop the pagination position via a shared canonicalizer, so a favorite pins the view's filters and tabs, and paging within a favorited view keeps it active. The span param stays: it pins a trace view selection that lives as long as the favorited run page itself. Both sides of the match are canonicalized so favorites saved before this keep working.
1 parent cbf3b0f commit 8a2eb70

2 files changed

Lines changed: 32 additions & 12 deletions

File tree

apps/webapp/app/components/navigation/FavoritePageButton.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { useShortcuts } from "../primitives/ShortcutsProvider";
1111
import { SimpleTooltip } from "../primitives/Tooltip";
1212
import {
1313
buildFavoriteLabel,
14+
canonicalFavoriteUrl,
1415
FAVORITE_SEARCH_PARAM,
1516
FAVORITES_ACTION_PATH,
17+
favoritePageUrl,
1618
resolvePageMeta,
17-
stripFavoriteSearchParam,
1819
useFavorites,
1920
} from "./favoritePages";
2021

@@ -37,8 +38,9 @@ export function FavoritePageButton({
3738
const { areShortcutsEnabled } = useShortcuts();
3839
const [, setSearchParams] = useSearchParams();
3940

40-
// The favorite marker param is presentation-only, so it never counts toward URL identity
41-
const url = location.pathname + stripFavoriteSearchParam(location.search);
41+
// The marker param and pagination position never count toward URL identity, so paging through
42+
// a favorited view keeps the same favorite (and never saves a soon-stale cursor)
43+
const url = favoritePageUrl(location.pathname, location.search);
4244

4345
// A marker that isn't one of this user's favorites came from a shared link (or a favorite
4446
// that's since been removed): clean it from the URL so the page behaves like a normal visit.
@@ -57,7 +59,7 @@ export function FavoritePageButton({
5759
{ replace: true, preventScrollReset: true }
5860
);
5961
}, [hasForeignMarker, setSearchParams]);
60-
const existing = favorites.find((favorite) => favorite.url === url);
62+
const existing = favorites.find((favorite) => canonicalFavoriteUrl(favorite.url) === url);
6163
const isFavorited = existing !== undefined;
6264
// The tooltip names the favorite: its custom name once saved, else the label saving would use
6365
// (which includes detail-page ids and filter summaries, e.g. "Runs: Completed, last 7d")

apps/webapp/app/components/navigation/favoritePages.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,36 @@ export function favoriteLinkTo(favorite: FavoritePage): string {
134134
return `${path}?${params.toString()}`;
135135
}
136136

137-
/** The current location's search string without the favorite marker, for saving/matching URLs. */
138-
export function stripFavoriteSearchParam(search: string): string {
137+
/** Pagination position params: never part of a favorite's identity (see favoritePageUrl). */
138+
const PAGINATION_PARAMS = ["cursor", "direction", "page"];
139+
140+
/**
141+
* The canonical URL a favorite saves and matches against: the path and search minus the favorite
142+
* marker (presentation-only) and the pagination position (cursors go stale, and page N of a view
143+
* is not a different view). A favorite pins filters and tabs, never a transient page of them.
144+
*/
145+
export function favoritePageUrl(pathname: string, search: string): string {
139146
const params = new URLSearchParams(search);
140147
params.delete(FAVORITE_SEARCH_PARAM);
148+
for (const param of PAGINATION_PARAMS) {
149+
params.delete(param);
150+
}
141151
const result = params.toString();
142-
return result.length > 0 ? `?${result}` : "";
152+
return pathname + (result.length > 0 ? `?${result}` : "");
153+
}
154+
155+
/** favoritePageUrl for an already-joined URL, e.g. a favorite's stored one (which may predate
156+
* pagination stripping). */
157+
export function canonicalFavoriteUrl(url: string): string {
158+
const [pathname, search = ""] = url.split("?");
159+
return favoritePageUrl(pathname, search);
143160
}
144161

145162
/**
146-
* A favorite is active only while the URL is exactly the view it saved: its marker param is
147-
* present AND the rest of the URL still matches. Changing any filter on the page diverges the
148-
* URL from the favorite, so it deactivates (and the regular menu item takes over).
163+
* A favorite is active only while the URL is the view it saved: its marker param is present AND
164+
* the canonical URL still matches. Changing any filter on the page diverges the URL from the
165+
* favorite, so it deactivates (and the regular menu item takes over) — but paging within the
166+
* view keeps it active, matching what the favorite pins.
149167
*/
150168
export function isFavoriteActive(
151169
favorite: FavoritePage,
@@ -154,7 +172,7 @@ export function isFavoriteActive(
154172
): boolean {
155173
return (
156174
new URLSearchParams(search).get(FAVORITE_SEARCH_PARAM) === favorite.id &&
157-
favorite.url === pathname + stripFavoriteSearchParam(search)
175+
canonicalFavoriteUrl(favorite.url) === favoritePageUrl(pathname, search)
158176
);
159177
}
160178

@@ -344,7 +362,7 @@ function humanizeValue(value: string): string {
344362
}
345363

346364
/** Pagination/UI-state params that never describe what the user filtered. */
347-
const NON_FILTER_PARAMS = [FAVORITE_SEARCH_PARAM, "cursor", "direction", "page", "span"];
365+
const NON_FILTER_PARAMS = [FAVORITE_SEARCH_PARAM, ...PAGINATION_PARAMS, "span"];
348366

349367
/**
350368
* Summarize a filtered view's search params into a short, selective descriptor for the favorite

0 commit comments

Comments
 (0)