diff --git a/templates/analytics/app/components/layout/Sidebar.tsx b/templates/analytics/app/components/layout/Sidebar.tsx index 809b12d1c5..e3ff7fe118 100644 --- a/templates/analytics/app/components/layout/Sidebar.tsx +++ b/templates/analytics/app/components/layout/Sidebar.tsx @@ -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 { @@ -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 { @@ -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"; @@ -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" > @@ -394,6 +405,20 @@ function SidebarSectionSettingsPopover({ > {t("sidebar.manual")} + + + + + + + + {t("sidebar.favoritesExplainer")} + +
@@ -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(null); @@ -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)); + } 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, diff --git a/templates/analytics/app/i18n-data.ts b/templates/analytics/app/i18n-data.ts index b65b928a75..6d57b19ed7 100644 --- a/templates/analytics/app/i18n-data.ts +++ b/templates/analytics/app/i18n-data.ts @@ -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.", @@ -4158,6 +4160,8 @@ export const messagesByLocale = { sortMostUsedPersonal: "按你在此浏览器中的打开次数排序", sortAlphabetically: "按字母排序", sortManually: "手动排序", + sortFavoritesFirst: "收藏优先排序", + favoritesExplainer: "将收藏的项目排在最前,其余按 A-Z 排列。", personalControlsNote: "排序、手动顺序和“常用”仅适用于此浏览器。", usedExplainer: "统计你在此浏览器中打开每个项目的次数。", visibilityAll: "全部", @@ -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: @@ -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: @@ -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: @@ -5058,6 +5070,8 @@ export const messagesByLocale = { sortMostUsedPersonal: "このブラウザーでの自分の表示回数で並べ替え", sortAlphabetically: "アルファベット順に並べ替え", sortManually: "手動で並べ替え", + sortFavoritesFirst: "お気に入りを先頭に並べ替え", + favoritesExplainer: "お気に入りを上部に表示し、残りは A-Z 順に並べます。", personalControlsNote: "並べ替え、手動順序、「使用」はこのブラウザーだけに保存されます。", usedExplainer: "このブラウザーで各項目を開いた回数を数えます。", @@ -5281,6 +5295,9 @@ export const messagesByLocale = { sortMostUsedPersonal: "이 브라우저에서 내가 연 횟수순으로 정렬", sortAlphabetically: "가나다순 정렬", sortManually: "수동 정렬", + sortFavoritesFirst: "즐겨찾기 먼저 정렬", + favoritesExplainer: + "즐겨찾기를 맨 위에 두고 나머지는 A-Z 순으로 정렬합니다.", personalControlsNote: "정렬, 수동 순서, 사용됨은 이 브라우저에만 적용됩니다.", usedExplainer: "이 브라우저에서 각 항목을 연 횟수를 계산합니다.", @@ -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: @@ -5735,6 +5755,8 @@ export const messagesByLocale = { sortMostUsedPersonal: "इस ब्राउज़र में आपके खोले गए आइटम के आधार पर क्रमबद्ध करें", sortAlphabetically: "वर्णानुक्रम में क्रमबद्ध करें", sortManually: "मैनुअल क्रमबद्ध करें", + sortFavoritesFirst: "पसंदीदा को पहले क्रमबद्ध करें", + favoritesExplainer: "आपके पसंदीदा ऊपर रहते हैं, बाकी A-Z क्रम में।", personalControlsNote: "क्रम, मैन्युअल क्रम और उपयोग इस ब्राउज़र के लिए निजी हैं।", usedExplainer: "गिनता है कि आपने इस ब्राउज़र में हर आइटम कितनी बार खोला है।", visibilityAll: "सभी", @@ -5955,6 +5977,8 @@ export const messagesByLocale = { sortMostUsedPersonal: "الترتيب حسب مرات فتحك في هذا المتصفح", sortAlphabetically: "ترتيب أبجدي", sortManually: "ترتيب يدوي", + sortFavoritesFirst: "ترتيب المفضلة أولا", + favoritesExplainer: "يضع المفضلة في الأعلى ثم الباقي بترتيب أبجدي.", personalControlsNote: "الفرز والترتيب اليدوي وخيار مستخدم شخصية لهذا المتصفح.", usedExplainer: "يحسب عدد مرات فتحك لكل عنصر في هذا المتصفح.", diff --git a/templates/analytics/app/i18n/zh-TW.ts b/templates/analytics/app/i18n/zh-TW.ts index 60c2af9846..b0086dcbb2 100644 --- a/templates/analytics/app/i18n/zh-TW.ts +++ b/templates/analytics/app/i18n/zh-TW.ts @@ -58,6 +58,8 @@ const messages = { sortMostUsedPersonal: "依你在此瀏覽器中的開啟次數排序", sortAlphabetically: "按字母排序", sortManually: "手動排序", + sortFavoritesFirst: "收藏優先排序", + favoritesExplainer: "將收藏的項目排在最前,其餘按 A-Z 排列。", personalControlsNote: "常用、排序和手動順序只會套用到此瀏覽器。", usedExplainer: "計算你在此瀏覽器中開啟每個項目的次數。", visibilityAll: "全部", diff --git a/templates/analytics/app/lib/dashboard-list-loading.spec.ts b/templates/analytics/app/lib/dashboard-list-loading.spec.ts index 885918d303..6b119f42e2 100644 --- a/templates/analytics/app/lib/dashboard-list-loading.spec.ts +++ b/templates/analytics/app/lib/dashboard-list-loading.spec.ts @@ -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); + }); }); diff --git a/templates/analytics/app/lib/dashboard-list-loading.ts b/templates/analytics/app/lib/dashboard-list-loading.ts index 166c73e804..206c52f04d 100644 --- a/templates/analytics/app/lib/dashboard-list-loading.ts +++ b/templates/analytics/app/lib/dashboard-list-loading.ts @@ -1,4 +1,8 @@ -export type DashboardSortMode = "most-used" | "alphabetical" | "manual"; +export type DashboardSortMode = + | "most-used" + | "alphabetical" + | "manual" + | "favorites"; export type DashboardListLoadingArgs = { sqlDashboardsLoading: boolean; @@ -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; } diff --git a/templates/analytics/changelog/2026-08-04-sort-the-sidebar-dashboard-list-by-your-favorites-from-the-s.md b/templates/analytics/changelog/2026-08-04-sort-the-sidebar-dashboard-list-by-your-favorites-from-the-s.md new file mode 100644 index 0000000000..5ef7f67d85 --- /dev/null +++ b/templates/analytics/changelog/2026-08-04-sort-the-sidebar-dashboard-list-by-your-favorites-from-the-s.md @@ -0,0 +1,6 @@ +--- +type: added +date: 2026-08-04 +--- + +Sort the sidebar dashboard list by your favorites from the section filter menu.