From 84207c1b9dcb1e7bf27f026bf10885c3f2a08a1a Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:33:14 +1000 Subject: [PATCH] fix infinite loop --- .../App/CommandMenu/RecentRoutes.tsx | 115 +++++++++++++++--- apps/studio/hooks/misc/useRecentRoutes.ts | 19 ++- 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/apps/studio/components/interfaces/App/CommandMenu/RecentRoutes.tsx b/apps/studio/components/interfaces/App/CommandMenu/RecentRoutes.tsx index c5edbb1f54b..63069dfbdc0 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/RecentRoutes.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/RecentRoutes.tsx @@ -1,39 +1,126 @@ import { History } from 'lucide-react' import { useRouter } from 'next/router' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useParams } from 'common' import { useRecentRoutes } from 'hooks/misc/useRecentRoutes' -import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' +import { + PageType, + useRegisterCommands, + useRegisterPage, + useSetCommandMenuOpen, + useSetPage, +} from 'ui-patterns/CommandMenu' import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' import { orderCommandSectionsByPriority } from './ordering' +const RECENTS_PAGE_NAME = 'Recent Pages' +const VISIBLE_RECENTS_COUNT = 3 + export function useRecentRoutesCommands() { const router = useRouter() const { ref } = useParams() const setIsOpen = useSetCommandMenuOpen() + const setPage = useSetPage() const { recentRoutes } = useRecentRoutes() - const commands = useMemo(() => { + // Stable navigation function + const navigateTo = useCallback( + (link: string) => { + router.push(link) + setIsOpen(false) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + + // Stable page navigation + const goToAllRecents = useCallback(() => { + setPage(RECENTS_PAGE_NAME) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Create command objects from recent routes - only depend on the route data, not callbacks + const allCommands = useMemo(() => { return recentRoutes.map((route) => ({ id: `recent-${route.key}`, name: route.childName, - // Include parent label in value for search value: route.parentLabel ? `${route.childName} ${route.parentLabel}` : route.childName, - action: () => { - router.push(route.link) - setIsOpen(false) - }, + link: route.link, + parentLabel: route.parentLabel, + })) + }, [recentRoutes]) + + // Commands shown in the main menu (first 3) + const visibleCommands = useMemo(() => { + return allCommands.slice(0, VISIBLE_RECENTS_COUNT) + }, [allCommands]) + + // Convert to ICommand format with actions + const allCommandsWithActions = useMemo(() => { + return allCommands.map((cmd) => ({ + id: cmd.id, + name: cmd.name, + value: cmd.value, + action: () => navigateTo(cmd.link), icon: () => , - badge: route.parentLabel - ? () => {route.parentLabel} + badge: cmd.parentLabel + ? () => {cmd.parentLabel} : undefined, })) - }, [recentRoutes, router, setIsOpen]) + }, [allCommands, navigateTo]) - useRegisterCommands(COMMAND_MENU_SECTIONS.RECENTS, commands, { - enabled: !!ref && commands.length > 0, - deps: [commands], + const visibleCommandsWithActions = useMemo(() => { + return visibleCommands.map((cmd) => ({ + id: cmd.id, + name: cmd.name, + value: cmd.value, + action: () => navigateTo(cmd.link), + icon: () => , + badge: cmd.parentLabel + ? () => {cmd.parentLabel} + : undefined, + })) + }, [visibleCommands, navigateTo]) + + // Register the "All recents" subpage with all commands + useRegisterPage( + RECENTS_PAGE_NAME, + { + type: PageType.Commands, + sections: [ + { + id: 'all-recents', + name: 'Recent Pages', + commands: allCommandsWithActions, + }, + ], + }, + { deps: [allCommandsWithActions], enabled: !!ref && allCommands.length > VISIBLE_RECENTS_COUNT } + ) + + // Register the visible commands + "All recents..." link + const commandsWithAllRecents = useMemo(() => { + const commands = [...visibleCommandsWithActions] + + // Add "All recents..." command if there are more than visible + if (allCommands.length > VISIBLE_RECENTS_COUNT) { + commands.push({ + id: 'all-recents', + name: 'All recents...', + value: 'All recents recent pages history', + action: goToAllRecents, + icon: () => , + badge: undefined, + }) + } + + return commands + }, [visibleCommandsWithActions, allCommands.length, goToAllRecents]) + + useRegisterCommands(COMMAND_MENU_SECTIONS.RECENTS, commandsWithAllRecents, { + enabled: !!ref && commandsWithAllRecents.length > 0, + deps: [commandsWithAllRecents], orderSection: orderCommandSectionsByPriority, sectionMeta: { priority: 1 }, }) diff --git a/apps/studio/hooks/misc/useRecentRoutes.ts b/apps/studio/hooks/misc/useRecentRoutes.ts index fd177f4e3d6..34bd3825e67 100644 --- a/apps/studio/hooks/misc/useRecentRoutes.ts +++ b/apps/studio/hooks/misc/useRecentRoutes.ts @@ -1,5 +1,5 @@ import { LOCAL_STORAGE_KEYS, useParams } from 'common' -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { useLocalStorageQuery } from './useLocalStorage' export type RecentRoute = { @@ -12,7 +12,7 @@ export type RecentRoute = { visitedAt: number // Timestamp for ordering } -const MAX_RECENT_ROUTES = 5 +const MAX_RECENT_ROUTES = 15 const EXCLUDED_ROUTE_KEYS = ['HOME'] // Don't track Project Overview since it's always visible const DEFAULT_RECENT_ROUTES: RecentRoute[] = [] @@ -20,12 +20,19 @@ const DEFAULT_RECENT_ROUTES: RecentRoute[] = [] export const useRecentRoutes = () => { const { ref } = useParams() const lastTrackedKeyRef = useRef(null) + const recentRoutesRef = useRef(DEFAULT_RECENT_ROUTES) const [recentRoutes, setRecentRoutes, { isSuccess }] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.RECENT_SIDEBAR_ROUTES(ref ?? ''), DEFAULT_RECENT_ROUTES ) + // Keep ref in sync with latest routes + useEffect(() => { + recentRoutesRef.current = recentRoutes + }, [recentRoutes]) + + // Stable trackRoute that uses ref for current routes const trackRoute = useCallback( (route: { key: string @@ -51,13 +58,15 @@ export const useRecentRoutes = () => { } lastTrackedKeyRef.current = route.key + const currentRoutes = recentRoutesRef.current + // Check if this route is already at the top - if so, don't update - if (recentRoutes.length > 0 && recentRoutes[0].key === route.key) { + if (currentRoutes.length > 0 && currentRoutes[0].key === route.key) { return } // Remove existing entry with the same key (to update visitedAt) - const filteredRoutes = recentRoutes.filter((r) => r.key !== route.key) + const filteredRoutes = currentRoutes.filter((r) => r.key !== route.key) // Add new entry at the beginning const newRoute: RecentRoute = { @@ -74,7 +83,7 @@ export const useRecentRoutes = () => { const newRoutes = [newRoute, ...filteredRoutes].slice(0, MAX_RECENT_ROUTES) setRecentRoutes(newRoutes) }, - [recentRoutes, setRecentRoutes] + [setRecentRoutes] ) // Sort by visitedAt descending (most recent first)