Skip to content
Open
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
49 changes: 38 additions & 11 deletions templates/analytics/app/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ type SidebarDashboard = {
parentId?: string;
};

function favoriteKeyOf(d: SidebarDashboard): string {
const resourceId = d.resourceId ?? d.id;
return d.source === "analysis" ? `analysis:${resourceId}` : d.id;
}

const SIDEBAR_SYNC_SETTLE_MS = 500;

function useSettledSyncVersion(version: number): number {
Expand Down Expand Up @@ -159,7 +164,7 @@ const SIDEBAR_COLLAPSE_KEY = "analytics.sidebar.collapsed";
const SIDEBAR_SKELETON_CLASS =
"bg-sidebar-foreground/12 dark:bg-sidebar-foreground/10";

type SidebarSortMode = "most-used" | "alphabetical" | "manual";
type SidebarSortMode = "most-used" | "alphabetical" | "manual" | "favorites";
type SidebarVisibilityFilter = "all" | "private" | "shared";

import {
Expand Down Expand Up @@ -208,7 +213,12 @@ function setStoredBoolean(key: string, value: boolean): void {
function getStoredSortMode(key: string): SidebarSortMode {
if (typeof window === "undefined") return "most-used";
const raw = window.localStorage.getItem(key);
if (raw === "alphabetical" || raw === "manual" || raw === "most-used") {
if (
raw === "alphabetical" ||
raw === "manual" ||
raw === "most-used" ||
raw === "favorites"
) {
return raw;
}
return "most-used";
Expand Down Expand Up @@ -359,12 +369,13 @@ function SidebarSectionSettingsPopover({
if (
next === "most-used" ||
next === "alphabetical" ||
next === "manual"
next === "manual" ||
next === "favorites"
) {
onSortModeChange(next);
}
}}
className="grid grid-cols-3 gap-1 rounded-lg border border-border/60 bg-background/50 p-1"
className="grid grid-cols-4 gap-1 rounded-lg border border-border/60 bg-background/50 p-1"
>
<Tooltip>
<TooltipTrigger asChild>
Expand Down Expand Up @@ -394,6 +405,20 @@ function SidebarSectionSettingsPopover({
>
{t("sidebar.manual")}
</ToggleGroupItem>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="favorites"
aria-label={t("sidebar.sortFavoritesFirst")}
className={cn(segmentedItemClass, "px-1")}
>
<IconStar className="h-3.5 w-3.5" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent side="bottom">
{t("sidebar.favoritesExplainer")}
</TooltipContent>
</Tooltip>
</ToggleGroup>
</div>
<div className="grid gap-1">
Expand Down Expand Up @@ -873,7 +898,7 @@ function SortableDashboardItem({
const resourceId = d.resourceId ?? d.id;
const href =
d.source === "analysis" ? `/analyses/${resourceId}` : `/dashboards/${d.id}`;
const favoriteKey = d.source === "analysis" ? `analysis:${resourceId}` : d.id;
const favoriteKey = favoriteKeyOf(d);
const t = useT();
const { mutateAsync: deleteView } = useDeleteDashboardView();
const [deletingViewId, setDeletingViewId] = useState<string | null>(null);
Expand Down Expand Up @@ -1703,15 +1728,17 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
if (dashboardSortMode === "manual" && dashboardOrderState.length > 0) {
return applyOrder(all, dashboardOrderState);
}
if (dashboardSortMode === "favorites") {
const favoriteRank = (d: SidebarDashboard) =>
favoriteIds.has(favoriteKeyOf(d)) ? 0 : 1;
// Stable sort keeps each group in alphabetical order.
return sortByName(all).sort((a, b) => favoriteRank(a) - favoriteRank(b));
Comment on lines +1731 to +1735

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.

🟡 Favorited nested dashboards can be hidden below non-favorites

The favorites comparator ranks each dashboard independently, but the later nesting step removes children from topLevelDashboards and renders them under their parent. A SQL dashboard can be favorited while its parent is not; in that case the child stays in the non-favorite parent group, and if that parent falls outside the five-item preview, the favorited child is not visible until “Show more.” Rank parent groups by whether they or any rendered child is favorited, or otherwise ensure favorited children remain accessible in the favorites section.

Additional Info
Found by 1 of 2 parallel code-review agents; browser confirmation was blocked by missing executor navigation tools.

Fix in Builder

}
return [...all].sort((a, b) => {
const aResourceId = a.resourceId ?? a.id;
const bResourceId = b.resourceId ?? b.id;
const aFavoriteKey =
a.source === "analysis" ? `analysis:${aResourceId}` : a.id;
const bFavoriteKey =
b.source === "analysis" ? `analysis:${bResourceId}` : b.id;
const aFav = favoriteIds.has(aFavoriteKey) ? 0 : 1;
const bFav = favoriteIds.has(bFavoriteKey) ? 0 : 1;
const aFav = favoriteIds.has(favoriteKeyOf(a)) ? 0 : 1;
const bFav = favoriteIds.has(favoriteKeyOf(b)) ? 0 : 1;
if (aFav !== bFav) return aFav - bFav;
const aPop = popularityOf(
popularity,
Expand Down
24 changes: 24 additions & 0 deletions templates/analytics/app/i18n-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ const enUS = {
sortMostUsedPersonal: "Sort by your opens in this browser",
sortAlphabetically: "Sort alphabetically",
sortManually: "Sort manually",
sortFavoritesFirst: "Sort favorites first",
favoritesExplainer: "Puts your favorites at the top, then the rest A-Z.",
personalControlsNote:
"Used, sort, and manual order are personal to this browser.",
usedExplainer: "Counts how often you've opened each item in this browser.",
Expand Down Expand Up @@ -4158,6 +4160,8 @@ export const messagesByLocale = {
sortMostUsedPersonal: "按你在此浏览器中的打开次数排序",
sortAlphabetically: "按字母排序",
sortManually: "手动排序",
sortFavoritesFirst: "收藏优先排序",
favoritesExplainer: "将收藏的项目排在最前,其余按 A-Z 排列。",
personalControlsNote: "排序、手动顺序和“常用”仅适用于此浏览器。",
usedExplainer: "统计你在此浏览器中打开每个项目的次数。",
visibilityAll: "全部",
Expand Down Expand Up @@ -4370,6 +4374,9 @@ export const messagesByLocale = {
sortMostUsedPersonal: "Ordenar por tus aperturas en este navegador",
sortAlphabetically: "Ordenar alfabéticamente",
sortManually: "Ordenar manualmente",
sortFavoritesFirst: "Ordenar favoritos primero",
favoritesExplainer:
"Coloca tus favoritos arriba y el resto en orden A-Z.",
personalControlsNote:
"La ordenación, el orden manual y Usado son personales de este navegador.",
usedExplainer:
Expand Down Expand Up @@ -4595,6 +4602,8 @@ export const messagesByLocale = {
sortMostUsedPersonal: "Trier selon vos ouvertures dans ce navigateur",
sortAlphabetically: "Trier alphabétiquement",
sortManually: "Trier manuellement",
sortFavoritesFirst: "Trier les favoris en premier",
favoritesExplainer: "Place vos favoris en haut, puis le reste de A à Z.",
personalControlsNote:
"Le tri, l'ordre manuel et Utilisé sont personnels à ce navigateur.",
usedExplainer:
Expand Down Expand Up @@ -4827,6 +4836,9 @@ export const messagesByLocale = {
sortMostUsedPersonal: "Nach deinen Öffnungen in diesem Browser sortieren",
sortAlphabetically: "Alphabetisch sortieren",
sortManually: "Manuell sortieren",
sortFavoritesFirst: "Favoriten zuerst sortieren",
favoritesExplainer:
"Stellt deine Favoriten nach oben, der Rest folgt von A-Z.",
personalControlsNote:
"Sortierung, manuelle Reihenfolge und Genutzt sind persönlich für diesen Browser.",
usedExplainer:
Expand Down Expand Up @@ -5058,6 +5070,8 @@ export const messagesByLocale = {
sortMostUsedPersonal: "このブラウザーでの自分の表示回数で並べ替え",
sortAlphabetically: "アルファベット順に並べ替え",
sortManually: "手動で並べ替え",
sortFavoritesFirst: "お気に入りを先頭に並べ替え",
favoritesExplainer: "お気に入りを上部に表示し、残りは A-Z 順に並べます。",
personalControlsNote:
"並べ替え、手動順序、「使用」はこのブラウザーだけに保存されます。",
usedExplainer: "このブラウザーで各項目を開いた回数を数えます。",
Expand Down Expand Up @@ -5281,6 +5295,9 @@ export const messagesByLocale = {
sortMostUsedPersonal: "이 브라우저에서 내가 연 횟수순으로 정렬",
sortAlphabetically: "가나다순 정렬",
sortManually: "수동 정렬",
sortFavoritesFirst: "즐겨찾기 먼저 정렬",
favoritesExplainer:
"즐겨찾기를 맨 위에 두고 나머지는 A-Z 순으로 정렬합니다.",
personalControlsNote:
"정렬, 수동 순서, 사용됨은 이 브라우저에만 적용됩니다.",
usedExplainer: "이 브라우저에서 각 항목을 연 횟수를 계산합니다.",
Expand Down Expand Up @@ -5504,6 +5521,9 @@ export const messagesByLocale = {
sortMostUsedPersonal: "Ordenar pelas suas aberturas neste navegador",
sortAlphabetically: "Ordenar alfabeticamente",
sortManually: "Ordenar manualmente",
sortFavoritesFirst: "Ordenar favoritos primeiro",
favoritesExplainer:
"Coloca seus favoritos no topo e o restante em ordem A-Z.",
personalControlsNote:
"A ordenação, a ordem manual e Usado são pessoais deste navegador.",
usedExplainer:
Expand Down Expand Up @@ -5735,6 +5755,8 @@ export const messagesByLocale = {
sortMostUsedPersonal: "इस ब्राउज़र में आपके खोले गए आइटम के आधार पर क्रमबद्ध करें",
sortAlphabetically: "वर्णानुक्रम में क्रमबद्ध करें",
sortManually: "मैनुअल क्रमबद्ध करें",
sortFavoritesFirst: "पसंदीदा को पहले क्रमबद्ध करें",
favoritesExplainer: "आपके पसंदीदा ऊपर रहते हैं, बाकी A-Z क्रम में।",
personalControlsNote: "क्रम, मैन्युअल क्रम और उपयोग इस ब्राउज़र के लिए निजी हैं।",
usedExplainer: "गिनता है कि आपने इस ब्राउज़र में हर आइटम कितनी बार खोला है।",
visibilityAll: "सभी",
Expand Down Expand Up @@ -5955,6 +5977,8 @@ export const messagesByLocale = {
sortMostUsedPersonal: "الترتيب حسب مرات فتحك في هذا المتصفح",
sortAlphabetically: "ترتيب أبجدي",
sortManually: "ترتيب يدوي",
sortFavoritesFirst: "ترتيب المفضلة أولا",
favoritesExplainer: "يضع المفضلة في الأعلى ثم الباقي بترتيب أبجدي.",
personalControlsNote:
"الفرز والترتيب اليدوي وخيار مستخدم شخصية لهذا المتصفح.",
usedExplainer: "يحسب عدد مرات فتحك لكل عنصر في هذا المتصفح.",
Expand Down
2 changes: 2 additions & 0 deletions templates/analytics/app/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const messages = {
sortMostUsedPersonal: "依你在此瀏覽器中的開啟次數排序",
sortAlphabetically: "按字母排序",
sortManually: "手動排序",
sortFavoritesFirst: "收藏優先排序",
favoritesExplainer: "將收藏的項目排在最前,其餘按 A-Z 排列。",
personalControlsNote: "常用、排序和手動順序只會套用到此瀏覽器。",
usedExplainer: "計算你在此瀏覽器中開啟每個項目的次數。",
visibilityAll: "全部",
Expand Down
26 changes: 26 additions & 0 deletions templates/analytics/app/lib/dashboard-list-loading.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,30 @@ describe("shouldRenderDashboardList", () => {
it("returns true when most-used sorting inputs are ready", () => {
expect(shouldRenderDashboardList(ready)).toBe(true);
});

it("waits for favorites before rendering favorites sorting", () => {
expect(
shouldRenderDashboardList({
sqlDashboardsLoading: false,
sqlDashboardsPlaceholder: false,
isInitialLoad: true,
favoritesLoading: true,
popularityReady: true,
sortMode: "favorites",
}),
).toBe(false);
});

it("ignores popularity readiness for favorites sorting", () => {
expect(
shouldRenderDashboardList({
sqlDashboardsLoading: false,
sqlDashboardsPlaceholder: false,
isInitialLoad: true,
favoritesLoading: false,
popularityReady: false,
sortMode: "favorites",
}),
).toBe(true);
});
});
12 changes: 9 additions & 3 deletions templates/analytics/app/lib/dashboard-list-loading.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export type DashboardSortMode = "most-used" | "alphabetical" | "manual";
export type DashboardSortMode =
| "most-used"
| "alphabetical"
| "manual"
| "favorites";

export type DashboardListLoadingArgs = {
sqlDashboardsLoading: boolean;
Expand All @@ -20,6 +24,8 @@ export function shouldRenderDashboardList({
if (sqlDashboardsLoading || (isInitialLoad && sqlDashboardsPlaceholder)) {
return false;
}
if (sortMode !== "most-used") return true;
return favoritesLoading === false && popularityReady;
const needsFavorites = sortMode === "most-used" || sortMode === "favorites";
if (needsFavorites && favoritesLoading) return false;
if (sortMode === "most-used" && !popularityReady) return false;
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: added
date: 2026-08-04
---

Sort the sidebar dashboard list by your favorites from the section filter menu.
Loading