diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index a6af214e9b4..64f7b13bd45 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -1,10 +1,11 @@ 'use client' -import { memo, type ReactElement, useEffect, useRef, useState } from 'react' +import { memo, type ReactElement, useEffect, useMemo, useRef, useState } from 'react' import { ChevronDown, Chip, ChipConfirmModal, + ChipInput, chipGeometryClass, chipVariants, cn, @@ -19,7 +20,7 @@ import { } from '@sim/emcn' import { ManageWorkspace, PanelLeft } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { MoreHorizontal } from 'lucide-react' +import { MoreHorizontal, Search } from 'lucide-react' import { useActiveOrganization } from '@/lib/auth/auth-client' import { isBillingEnabled } from '@/lib/core/config/env-flags' import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu' @@ -35,6 +36,9 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation' const logger = createLogger('WorkspaceHeader') +/** Show the search input once the workspace list exceeds this count. */ +const WORKSPACE_SEARCH_THRESHOLD = 3 + /** * Derives the single-letter avatar initial for a workspace, ignoring the word * "workspace" in the name (e.g. "Acme Workspace" → "A"). @@ -150,6 +154,25 @@ function WorkspaceHeaderImpl({ const contextMenuClosedRef = useRef(true) const hasInputFocusedRef = useRef(false) const renameInputRef = useRef(null) + const searchInputRef = useRef(null) + const workspaceListRef = useRef(null) + + const [workspaceSearch, setWorkspaceSearch] = useState('') + const [highlightedIndex, setHighlightedIndex] = useState(0) + + const showSearch = workspaces.length > WORKSPACE_SEARCH_THRESHOLD + const filteredWorkspaces = useMemo(() => { + const q = workspaceSearch.trim().toLowerCase() + return q ? workspaces.filter((w) => w.name.toLowerCase().includes(q)) : workspaces + }, [workspaceSearch, workspaces]) + + useEffect(() => { + if (!showSearch || !isWorkspaceMenuOpen) return + const el = workspaceListRef.current?.querySelector( + `[data-workspace-row-idx="${highlightedIndex}"]` + ) + el?.scrollIntoView({ block: 'nearest' }) + }, [highlightedIndex, showSearch, isWorkspaceMenuOpen]) const [isMounted, setIsMounted] = useState(false) useEffect(() => { @@ -361,6 +384,13 @@ function WorkspaceHeaderImpl({ return } setIsWorkspaceMenuOpen(open) + if (open && showSearch) { + requestAnimationFrame(() => searchInputRef.current?.focus()) + } + if (!open && showSearch) { + setWorkspaceSearch('') + setHighlightedIndex(0) + } }} > @@ -422,14 +452,57 @@ function WorkspaceHeaderImpl({ ) : ( <> -
- {workspaces.map((workspace) => { + {showSearch && ( + { + setWorkspaceSearch(e.target.value) + setHighlightedIndex(0) + }} + onKeyDown={(e) => { + e.stopPropagation() + if (filteredWorkspaces.length === 0) return + if (e.key === 'ArrowDown') { + e.preventDefault() + setHighlightedIndex((i) => (i + 1) % filteredWorkspaces.length) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setHighlightedIndex( + (i) => (i - 1 + filteredWorkspaces.length) % filteredWorkspaces.length + ) + } else if (e.key === 'Enter') { + e.preventDefault() + const target = filteredWorkspaces[highlightedIndex] + if (target) onWorkspaceSwitch(target) + } + }} + className='mb-1.5' + /> + )} +
+ {filteredWorkspaces.length === 0 && workspaceSearch && ( +
+ No results for "{workspaceSearch}" +
+ )} + {filteredWorkspaces.map((workspace, idx) => { const initial = getWorkspaceInitial(workspace.name) const isActive = workspace.id === workspaceId const isMenuOpen = menuOpenWorkspaceId === workspace.id + const isKeyboardHighlighted = showSearch && idx === highlightedIndex return ( -
+
setHighlightedIndex(idx) : undefined} + > {editingWorkspaceId === workspace.id ? (