Skip to main content
Glama
index.tsx12.5 kB
import { useQueryClient } from '@tanstack/react-query'; import { t } from 'i18next'; import { Compass, Search, Loader2, Plus } from 'lucide-react'; import { useState, useMemo, useRef, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useDebounce } from 'use-debounce'; import { NewProjectDialog } from '@/app/routes/platform/projects/new-project-dialog'; import { useEmbedding } from '@/components/embed-provider'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarMenu, SidebarGroupContent, SidebarSeparator, useSidebar, SidebarGroupLabel, } from '@/components/ui/sidebar-shadcn'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { platformHooks } from '@/hooks/platform-hooks'; import { projectHooks } from '@/hooks/project-hooks'; import { userHooks } from '@/hooks/user-hooks'; import { cn } from '@/lib/utils'; import { isNil, PlatformRole, ProjectType, TeamProjectsLimit, } from '@activepieces/shared'; import { SidebarGeneralItemType } from '../ap-sidebar-group'; import { ApSidebarItem, SidebarItemType } from '../ap-sidebar-item'; import ProjectSideBarItem from '../project'; import { AppSidebarHeader } from '../sidebar-header'; import SidebarUsageLimits from '../sidebar-usage-limits'; import { SidebarUser } from '../sidebar-user'; export function ProjectDashboardSidebar() { const { data: projectPages, fetchNextPage, hasNextPage, isFetchingNextPage, refetch: refetchProjects, } = projectHooks.useProjectsInfinite(20); const { embedState } = useEmbedding(); const { state, setOpen } = useSidebar(); const location = useLocation(); const [searchQuery, setSearchQuery] = useState(''); const [debouncedSearchQuery] = useDebounce(searchQuery, 300); const [searchOpen, setSearchOpen] = useState(false); const navigate = useNavigate(); const queryClient = useQueryClient(); const { setCurrentProject } = projectHooks.useCurrentProject(); const projectsScrollRef = useRef<HTMLDivElement>(null); const { data: currentUser } = userHooks.useCurrentUser(); const { platform } = platformHooks.useCurrentPlatform(); const { data: searchResults, isLoading: isSearching, refetch: refetchSearchResults, } = projectHooks.useProjects({ displayName: debouncedSearchQuery, limit: 100, }); useEffect(() => { if (!searchOpen) { setSearchQuery(''); } }, [searchOpen]); const allProjects = useMemo(() => { const projects = projectPages?.pages.flatMap((page) => page.data) ?? []; const uniqueProjects = Array.from( new Map(projects.map((project) => [project.id, project])).values(), ); return uniqueProjects; }, [projectPages]); const shouldShowNewProjectButton = useMemo(() => { if (platform.plan.teamProjectsLimit === TeamProjectsLimit.NONE) { return false; } return currentUser?.platformRole === PlatformRole.ADMIN; }, [platform.plan.teamProjectsLimit]); const shouldShowSearchButton = useMemo(() => { if (platform.plan.teamProjectsLimit === TeamProjectsLimit.NONE) { return false; } return true; }, [platform.plan.teamProjectsLimit]); const shouldDisableNewProjectButton = useMemo(() => { if (platform.plan.teamProjectsLimit === TeamProjectsLimit.ONE) { const teamProjects = allProjects.filter( (project) => project.type === ProjectType.TEAM, ); return teamProjects.length >= 1; } return false; }, [platform.plan.teamProjectsLimit, allProjects]); const isSearchMode = debouncedSearchQuery.length > 0; const displayProjects = useMemo(() => { if (isSearchMode) { return searchResults ?? []; } return allProjects; }, [isSearchMode, searchResults, allProjects]); useEffect(() => { const scrollContainer = projectsScrollRef.current; if (!scrollContainer || isSearchMode) return; const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const scrollThreshold = 100; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; if ( distanceFromBottom < scrollThreshold && hasNextPage && !isFetchingNextPage ) { fetchNextPage(); } }; scrollContainer.addEventListener('scroll', handleScroll); handleScroll(); return () => scrollContainer.removeEventListener('scroll', handleScroll); }, [hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode]); const permissionFilter = (link: SidebarGeneralItemType) => { if (link.type === 'link') { return isNil(link.hasPermission) || link.hasPermission; } return true; }; const exploreLink: SidebarItemType = { type: 'link', to: '/explore', label: t('Explore'), show: true, icon: Compass, hasPermission: true, isSubItem: false, }; const items = [exploreLink].filter(permissionFilter); const handleProjectSelect = async (projectId: string) => { const project = displayProjects?.find((p) => p.id === projectId); if (project) { await setCurrentProject(queryClient, project); navigate('/'); setSearchOpen(false); } }; return ( !embedState.hideSideNav && ( <Sidebar variant="inset" collapsible="icon" onClick={() => setOpen(true)} className={cn( state === 'collapsed' ? 'cursor-nesw-resize' : '', 'group', 'p-1', )} > <AppSidebarHeader /> {state === 'collapsed' && <div className="mt-1" />} {state === 'expanded' && <div className="mt-2" />} <SidebarContent className={cn( state === 'collapsed' ? 'gap-2' : 'gap-0', 'scrollbar-hover', 'cursor-default', 'flex', 'flex-col', 'overflow-hidden', )} > <SidebarGroup className="cursor-default flex-shrink-0"> <SidebarGroupContent> <SidebarMenu> {items.map((item) => ( <ApSidebarItem key={item.label} {...item} /> ))} </SidebarMenu> </SidebarGroupContent> </SidebarGroup> <SidebarSeparator className={cn( state === 'collapsed' ? 'mb-3' : 'mb-5', 'flex-shrink-0', )} /> <SidebarGroup className="flex-1 flex flex-col overflow-hidden"> {state === 'expanded' && ( <div className="flex items-center justify-between"> <SidebarGroupLabel>{t('Projects')}</SidebarGroupLabel> <div className="flex items-center gap-1"> {shouldShowNewProjectButton && ( <> {!shouldDisableNewProjectButton ? ( <NewProjectDialog onCreate={() => { refetchProjects(); refetchSearchResults(); }} > <Button variant="ghost" size="icon" className="h-6 w-6 hover:bg-accent" > <Plus /> </Button> </NewProjectDialog> ) : ( <Tooltip> <TooltipTrigger asChild> <div> <Button variant="ghost" size="icon" disabled className="h-6 w-6" > <Plus /> </Button> </div> </TooltipTrigger> <TooltipContent className="max-w-[250px]"> <p className="text-xs mb-1"> {t( 'Upgrade your plan to create additional team projects.', )}{' '} <button className="text-xs text-primary underline hover:no-underline" onClick={() => window.open( 'https://www.activepieces.com/pricing', '_blank', ) } > {t('View Plans')} </button> </p> </TooltipContent> </Tooltip> )} </> )} {shouldShowSearchButton && ( <Popover open={searchOpen} onOpenChange={setSearchOpen}> <PopoverTrigger asChild> <Button variant="ghost" size="icon" className="h-6 w-6 hover:bg-accent" > <Search /> </Button> </PopoverTrigger> <PopoverContent className="w-[280px] p-3" align="start" side="right" sideOffset={8} > <Input placeholder={t('Search projects...')} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="h-9" autoFocus /> </PopoverContent> </Popover> )} </div> </div> )} <div ref={projectsScrollRef} className={cn( 'flex-1 overflow-y-auto', state === 'collapsed' ? 'flex flex-col items-center scrollbar-none' : 'scrollbar-hover', )} onClick={(e) => e.stopPropagation()} > <SidebarMenu className={cn( state === 'collapsed' ? 'gap-2 flex flex-col items-center' : '', )} > {displayProjects.map((project) => ( <ProjectSideBarItem key={project.id} project={project} isCurrentProject={location.pathname.includes( `/projects/${project.id}`, )} handleProjectSelect={handleProjectSelect} /> ))} {(isFetchingNextPage || (isSearchMode && isSearching)) && ( <div className="flex items-center gap-2 px-2 py-2 text-sm text-muted-foreground"> <Loader2 className="h-4 w-4 animate-spin" /> {state === 'expanded' && <span>{t('Loading...')}</span>} </div> )} {isSearchMode && !isSearching && displayProjects.length === 0 && ( <div className="px-2 py-2 text-sm text-muted-foreground"> {state === 'expanded' && t('No projects found.')} </div> )} </SidebarMenu> </div> </SidebarGroup> </SidebarContent> <SidebarFooter onClick={(e) => e.stopPropagation()} className="cursor-default" > {state === 'expanded' && ( <div className="mb-2"> <SidebarUsageLimits /> </div> )} <SidebarUser /> </SidebarFooter> </Sidebar> ) ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/activepieces/activepieces'

If you have feedback or need assistance with the MCP directory API, please join our Discord server