import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/src/components/ui/command"; import { useRouter } from "next/router"; import { useEffect, memo } from "react"; import { useSession } from "next-auth/react"; import { env } from "@/src/env.mjs"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import { useDebounce } from "@/src/hooks/useDebounce"; import { useCommandMenu } from "@/src/features/command-k-menu/CommandMenuProvider"; import { useProjectSettingsPages } from "@/src/pages/project/[projectId]/settings"; import { useOrganizationSettingsPages } from "@/src/pages/organization/[organizationId]/settings"; import { useAccountSettingsPages } from "@/src/pages/account/settings"; import { useQueryProjectOrOrganization } from "@/src/features/projects/hooks"; import { api } from "@/src/utils/api"; import { type NavigationItem } from "@/src/components/layouts/utilities/routes"; function MainNavigationGroup({ navItems, onNavigate, }: { navItems: Array<{ title: string; url: string }>; onNavigate: (item: { title: string; url: string }) => void; }) { const router = useRouter(); const capture = usePostHogClientCapture(); return ( {navItems.map((item) => ( { router.push(item.url); capture("cmd_k_menu:navigated", { type: "main_navigation", title: item.title, url: item.url, }); onNavigate(item); }} > {item.title} ))} ); } function ProjectsGroup({ onNavigate }: { onNavigate: () => void }) { const router = useRouter(); const capture = usePostHogClientCapture(); const { allProjectItems } = useNavigationItems(); if (allProjectItems.length === 0) return null; return ( <> {allProjectItems.map((item) => ( { router.push(item.url); capture("cmd_k_menu:navigated", { type: "project", title: item.title, url: item.url, }); onNavigate(); }} > {item.title} ))} ); } function DashboardsGroup({ onNavigate }: { onNavigate: () => void }) { const router = useRouter(); const capture = usePostHogClientCapture(); const { project } = useQueryProjectOrOrganization(); const { open } = useCommandMenu(); const dashboardsQuery = api.dashboard.allDashboards.useQuery( { projectId: project?.id ?? "", orderBy: { column: "updatedAt", order: "DESC", }, limit: 100, page: 0, }, { enabled: open && Boolean(project?.id), }, ); const dashboards = dashboardsQuery.data?.dashboards ?? []; if (dashboards.length === 0) return null; return ( <> {dashboards.map((dashboard) => ( ${dashboard.name}`} keywords={[ "dashboard", dashboard.name.toLowerCase(), (dashboard.description ?? "").toLowerCase(), ]} disabled={router.query.dashboardId === dashboard.id} onSelect={() => { const url = `/project/${project?.id}/dashboards/${dashboard.id}`; router.push(url); capture("cmd_k_menu:navigated", { type: "dashboard", title: `Dashboard > ${dashboard.name}`, url: url, }); onNavigate(); }} > {dashboard.name} ))} ); } function ProjectSettingsGroup({ onNavigate }: { onNavigate: () => void }) { const router = useRouter(); const capture = usePostHogClientCapture(); const settingsPages = useProjectSettingsPages(); const { project } = useQueryProjectOrOrganization(); const projectSettingsItems = settingsPages .filter((page) => page.show !== false && !("href" in page)) .map((page) => ({ title: `Project Settings > ${page.title}`, url: `/project/${project?.id}/settings${page.slug === "index" ? "" : `/${page.slug}`}`, keywords: page.cmdKKeywords || [], })); if (projectSettingsItems.length === 0) return null; return ( <> {projectSettingsItems.map((item) => ( { router.push(item.url); capture("cmd_k_menu:navigated", { type: "project_settings", title: item.title, url: item.url, }); onNavigate(); }} > {item.title} ))} ); } function OrganizationSettingsGroup({ onNavigate }: { onNavigate: () => void }) { const router = useRouter(); const capture = usePostHogClientCapture(); const orgSettingsPages = useOrganizationSettingsPages(); const { organization } = useQueryProjectOrOrganization(); const orgSettingsItems = orgSettingsPages .filter((page) => page.show !== false && !("href" in page)) .map((page) => ({ title: `Organization Settings > ${page.title}`, url: `/organization/${organization?.id}/settings${page.slug === "index" ? "" : `/${page.slug}`}`, keywords: page.cmdKKeywords || [], })); if (orgSettingsItems.length === 0) return null; return ( <> {orgSettingsItems.map((item) => ( { router.push(item.url); capture("cmd_k_menu:navigated", { type: "organization_settings", title: item.title, url: item.url, }); onNavigate(); }} > {item.title} ))} ); } function AccountSettingsGroup({ onNavigate }: { onNavigate: () => void }) { const router = useRouter(); const capture = usePostHogClientCapture(); const accountSettingsPages = useAccountSettingsPages(); const accountSettingsItems = accountSettingsPages.map((page) => ({ title: `Account Settings > ${page.title}`, url: `/account/settings${page.slug === "index" ? "" : `/${page.slug}`}`, keywords: page.cmdKKeywords || [], })); if (accountSettingsItems.length === 0) return null; return ( <> {accountSettingsItems.map((item) => ( { router.push(item.url); capture("cmd_k_menu:navigated", { type: "account_settings", title: item.title, url: item.url, }); onNavigate(); }} > {item.title} ))} ); } function CommandMenuComponent({ mainNavigation, }: { mainNavigation: NavigationItem[]; }) { const { open, setOpen } = useCommandMenu(); const capture = usePostHogClientCapture(); const debouncedSearchChange = useDebounce( (value: string) => { capture("cmd_k_menu:search_entered", { search: value, }); }, 500, false, ); const navItems = mainNavigation .flatMap((item) => { if (item.items) { // if the item has children, return the children and not the parent return item.items.map((child) => ({ title: `${item.title} > ${child.title}`, url: child.url, })); } return [ { title: item.title, url: item.url, }, ]; }) .filter((item) => Boolean(item.url) && !item.url.includes("[")); // Keyboard shortcut effect useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!open) { capture("cmd_k_menu:opened", { source: "cmd_k", }); } setOpen(!open); } }; document.addEventListener("keydown", down); return () => document.removeEventListener("keydown", down); }, [capture, setOpen, open]); const handleNavigate = () => { setOpen(false); }; return ( { const extendValue = value + " " + keywords?.join(" "); const searchTerms = search.toLowerCase().split(" "); return searchTerms.every((term) => extendValue.toLowerCase().includes(term), ) ? 1 : 0; }} > No results found. ); } export const CommandMenu = memo( CommandMenuComponent, (prevProps, nextProps) => { // Only re-render if mainNavigation titles or urls change if (prevProps.mainNavigation.length !== nextProps.mainNavigation.length) { return false; } const isSame = prevProps.mainNavigation.every((item, idx) => { const nextItem = nextProps.mainNavigation[idx]; const itemTitleUrl = item.title === nextItem.title && item.url === nextItem.url; if (!itemTitleUrl) { return false; } // Check children if they exist if (item.items && nextItem.items) { if (item.items.length !== nextItem.items.length) { return false; } const childrenMatch = item.items.every((child, childIdx) => { const nextChild = nextItem.items![childIdx]; const match = child.title === nextChild.title && child.url === nextChild.url; return match; }); return itemTitleUrl && childrenMatch; } if ((item.items || nextItem.items) && !(item.items && nextItem.items)) { return false; } return itemTitleUrl && !item.items && !nextItem.items; }); return isSame; }, ); export const useNavigationItems = () => { const router = useRouter(); const session = useSession(); const organizations = session.data?.user?.organizations; const truncatePathBeforeDynamicSegments = (path: string) => { const allowlistedIds = ["[projectId]", "[organizationId]", "[page]"]; const segments = router.route.split("/"); const idSegments = segments.filter( (segment) => segment.startsWith("[") && segment.endsWith("]"), ); const stopSegment = idSegments.filter((id) => !allowlistedIds.includes(id)); if (stopSegment.length === 0) return path; const stopIndex = segments.indexOf(stopSegment[0]); const truncatedPath = path.split("/").slice(0, stopIndex).join("/"); return truncatedPath; }; const getProjectPath = (projectId: string) => router.query.projectId ? truncatePathBeforeDynamicSegments(router.asPath).replace( router.query.projectId as string, projectId, ) : `/project/${projectId}`; const allProjectItems = organizations ? organizations .sort((a, b) => { // sort demo org to the bottom const isDemoA = env.NEXT_PUBLIC_DEMO_ORG_ID === a.id; const isDemoB = env.NEXT_PUBLIC_DEMO_ORG_ID === b.id; if (isDemoA) return 1; if (isDemoB) return -1; return a.name.localeCompare(b.name); }) .flatMap((org) => org.projects.map((proj) => ({ title: `${org.name} > ${proj.name}`, url: getProjectPath(proj.id), active: router.query.projectId === proj.id, keywords: [ "project", org.name.toLowerCase(), proj.name.toLowerCase(), ], })), ) : []; return { allProjectItems, isLoading: !organizations, }; };