Skip to main content
Glama

mcp-google-sheets

flows-navigation.tsx11.4 kB
import { useQueryClient } from '@tanstack/react-query'; import { t } from 'i18next'; import { EllipsisVertical, Folder, FolderOpen, Shapes } from 'lucide-react'; import { useMemo, useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { ScrollArea } from '@/components/ui/scroll-area'; import { SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarSkeleton, } from '@/components/ui/sidebar-shadcn'; import { CreateFlowDropdown } from '@/features/flows/lib/create-flow-dropdown'; import { flowsHooks } from '@/features/flows/lib/flows-hooks'; import { CreateFolderDialog } from '@/features/folders/component/create-folder-dialog'; import { FolderActions } from '@/features/folders/component/folder-actions'; import { foldersHooks } from '@/features/folders/lib/folders-hooks'; import { authenticationSession } from '@/lib/authentication-session'; import { cn } from '@/lib/utils'; import { FolderDto, PopulatedFlow } from '@activepieces/shared'; import FlowActionMenu from '../../flow-actions-menu'; interface FlowsByFolder { [folderId: string]: PopulatedFlow[]; } export function FlowsNavigation() { const navigate = useNavigate(); const { flowId: currentFlowId } = useParams(); const scrollAreaRef = useRef<HTMLDivElement>(null); const [openFolders, setOpenFolders] = useState<Set<string>>(new Set()); const [previousFlowCount, setPreviousFlowCount] = useState<number>(0); const { folders, isLoading: foldersLoading, refetch: refetchFolders, } = foldersHooks.useFolders(); const { data: flows, isLoading: flowsLoading, refetch: refetchFlows, } = flowsHooks.useFlows({ cursor: undefined, limit: 99999, }); const flowsData = flows?.data || []; const flowsByFolder = flowsData.reduce<FlowsByFolder>((acc, flow) => { const folderId = flow.folderId || 'default'; if (!acc[folderId]) { acc[folderId] = []; } acc[folderId].push(flow); return acc; }, {}); const currentFlowFolderId = useMemo(() => { if (!currentFlowId || !flowsData.length) return null; const currentFlow = flowsData.find((flow) => flow.id === currentFlowId); return currentFlow ? currentFlow.folderId || 'default' : null; }, [currentFlowId, flowsData]); useEffect(() => { if (currentFlowFolderId && !flowsLoading) { setOpenFolders((prev) => new Set([...prev, currentFlowFolderId])); } }, [currentFlowFolderId, flowsLoading]); useEffect(() => { if (flowsLoading) return; const currentFlowCount = flowsData.length; if (currentFlowCount > previousFlowCount && previousFlowCount > 0) { const newFlow = flowsData[flowsData.length - 1]; if (newFlow) { const newFlowFolderId = newFlow.folderId || 'default'; setOpenFolders((prev) => new Set([...prev, newFlowFolderId])); } } setPreviousFlowCount(currentFlowCount); }, [flowsData.length, previousFlowCount, flowsLoading, flowsData]); useEffect(() => { if (!currentFlowId || foldersLoading || flowsLoading) return; const timeoutId = setTimeout(() => { const selectedFlowElement = document.querySelector( `[data-flow-id="${currentFlowId}"]`, ); if (selectedFlowElement && scrollAreaRef.current) { const scrollContainer = scrollAreaRef.current.querySelector( '[data-radix-scroll-area-viewport]', )!; const containerRect = scrollContainer.getBoundingClientRect(); const elementRect = selectedFlowElement.getBoundingClientRect(); if ( elementRect.top < containerRect.top || elementRect.bottom > containerRect.bottom ) { selectedFlowElement.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest', }); } } }, 100); return () => clearTimeout(timeoutId); }, [currentFlowId, foldersLoading, flowsLoading, currentFlowFolderId]); const handleFlowClick = (flowId: string) => { navigate( authenticationSession.appendProjectRoutePrefix(`/flows/${flowId}`), ); }; const handleFolderToggle = (folderId: string, isOpen: boolean) => { setOpenFolders((prev) => { const newSet = new Set(prev); if (isOpen) { newSet.add(folderId); } else { newSet.delete(folderId); } return newSet; }); }; const sortedFolders = folders?.sort((a, b) => a.displayName.localeCompare(b.displayName)) || []; const defaultFolderFlows = flowsByFolder['default'] || []; if (foldersLoading || flowsLoading) { return ( <SidebarGroup> <SidebarGroupLabel>{t('Flows Folders')}</SidebarGroupLabel> <SidebarGroupContent> <SidebarSkeleton numOfItems={6} /> </SidebarGroupContent> </SidebarGroup> ); } return ( <SidebarGroup className="pb-2 max-h-[calc(50%-10px)] pr-0"> <SidebarGroupLabel className="flex px-2 font-semibold text-foreground text-sm justify-between items-center w-full mb-1"> {t('Flows')} <CreateFolderDialog refetchFolders={refetchFolders} updateSearchParams={() => {}} /> </SidebarGroupLabel> <ScrollArea ref={scrollAreaRef} showGradient> <SidebarGroupContent> <SidebarMenu className="pr-2"> <DefaultFolder flows={defaultFolderFlows} onFlowClick={handleFlowClick} isFlowActive={(flowId) => currentFlowId === flowId} isOpen={openFolders.has('default')} onToggle={(isOpen) => handleFolderToggle('default', isOpen)} refetch={refetchFlows} /> {sortedFolders.map((folder) => ( <RegularFolder key={folder.id} folder={folder} flows={flowsByFolder[folder.id] || []} onFlowClick={handleFlowClick} isFlowActive={(flowId) => currentFlowId === flowId} isOpen={openFolders.has(folder.id)} onToggle={(isOpen) => handleFolderToggle(folder.id, isOpen)} refetch={refetchFlows} refetchFolders={refetchFolders} /> ))} </SidebarMenu> </SidebarGroupContent> </ScrollArea> </SidebarGroup> ); } interface FolderProps { flows: PopulatedFlow[]; onFlowClick: (flowId: string) => void; isFlowActive: (flowId: string) => boolean; isOpen: boolean; onToggle: (isOpen: boolean) => void; refetch: () => void; } function DefaultFolder({ flows, onFlowClick, isFlowActive, isOpen, onToggle, refetch, }: FolderProps) { return ( <Collapsible open={isOpen} onOpenChange={onToggle} className="group/collapsible" > <SidebarMenuItem> <CollapsibleTrigger asChild> <SidebarMenuButton className="px-2 group/item mb-1 pr-0"> <Shapes className="!size-3.5" /> <span>{t('Uncategorized')}</span> <div className="ml-auto relative"> <CreateFlowDropdown folderId="NULL" variant="small" className="opacity-0 group-hover/item:opacity-100" refetch={refetch} /> </div> </SidebarMenuButton> </CollapsibleTrigger> {flows.length > 0 && ( <CollapsibleContent> <SidebarMenuSub> {flows.map((flow) => ( <FlowItem key={flow.id} flow={flow} isActive={isFlowActive(flow.id)} onClick={() => onFlowClick(flow.id)} refetch={refetch} /> ))} </SidebarMenuSub> </CollapsibleContent> )} </SidebarMenuItem> </Collapsible> ); } interface RegularFolderProps extends FolderProps { folder: FolderDto; refetchFolders: () => void; } function RegularFolder({ folder, flows, onFlowClick, isFlowActive, isOpen, onToggle, refetch, refetchFolders, }: RegularFolderProps) { return ( <Collapsible open={isOpen} onOpenChange={onToggle} className="group/collapsible" > <SidebarMenuItem> <CollapsibleTrigger asChild> <SidebarMenuButton className="px-2 group/item mb-1 pr-0"> <Folder className="!size-3.5 group-data-[state=open]/collapsible:hidden" /> <FolderOpen className="!size-3.5 hidden group-data-[state=open]/collapsible:block" /> <span className="truncate">{folder.displayName}</span> <div className="flex items-center justify-center ml-auto"> <CreateFlowDropdown folderId={folder.id} variant="small" className="group-hover/item:opacity-100 opacity-0" refetch={refetch} /> <FolderActions hideFlowCount={true} refetch={refetchFolders} folder={folder} /> </div> </SidebarMenuButton> </CollapsibleTrigger> {flows.length > 0 && ( <CollapsibleContent> <SidebarMenuSub> {flows.map((flow) => ( <FlowItem key={flow.id} flow={flow} isActive={isFlowActive(flow.id)} onClick={() => onFlowClick(flow.id)} refetch={refetch} /> ))} </SidebarMenuSub> </CollapsibleContent> )} </SidebarMenuItem> </Collapsible> ); } interface FlowItemProps { flow: PopulatedFlow; isActive: boolean; onClick: () => void; refetch: () => void; } function FlowItem({ flow, isActive, onClick, refetch }: FlowItemProps) { const { flowId } = useParams(); const queryClient = useQueryClient(); return ( <SidebarMenuSubItem className="cursor-pointer group/item" data-flow-id={flow.id} > <SidebarMenuSubButton onClick={onClick} className={cn(isActive && 'bg-sidebar-accent', 'pr-0 pl-2')} > <span className="truncate">{flow.version.displayName}</span> <FlowActionMenu insideBuilder={false} flow={flow} readonly={false} flowVersion={flow.version} onRename={refetch} onMoveTo={refetch} onDelete={() => { if (flowId === flow.id) { flowsHooks.invalidateFlowsQuery(queryClient); } refetch(); }} onDuplicate={refetch} > <Button variant="ghost" size="icon" className="ml-auto group-hover/item:opacity-100 opacity-0" onClick={(e) => e.stopPropagation()} > <EllipsisVertical className="size-4" /> </Button> </FlowActionMenu> </SidebarMenuSubButton> </SidebarMenuSubItem> ); }

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