+
+
+ {/* Icon skeleton */}
+
+ {/* Title skeleton */}
+
+
+
+ {/* Star and Use button skeleton */}
+
{/* Description skeleton */}
-
{/* Bottom section skeleton */}
-
-
+
+
-
+
+ {/* Stars section - hidden on smaller screens */}
+
- {/* Right side - Blocks skeleton */}
-
- {Array.from({ length: 4 }).map((_, index) => (
-
+ {/* Right side - Block Icons skeleton */}
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
))}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx
index 8171d157b5..e6d842a81d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
-import { BookOpen, LibraryBig, ScrollText, Search, Shapes } from 'lucide-react'
+import { BookOpen, Building2, LibraryBig, ScrollText, Search, Shapes, Workflow } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Dialog, DialogOverlay, DialogPortal, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
@@ -15,7 +15,10 @@ interface SearchModalProps {
open: boolean
onOpenChange: (open: boolean) => void
templates?: TemplateData[]
+ workflows?: WorkflowItem[]
+ workspaces?: WorkspaceItem[]
loading?: boolean
+ isOnWorkflowPage?: boolean
}
interface TemplateData {
@@ -33,6 +36,20 @@ interface TemplateData {
isStarred?: boolean
}
+interface WorkflowItem {
+ id: string
+ name: string
+ href: string
+ isCurrent?: boolean
+}
+
+interface WorkspaceItem {
+ id: string
+ name: string
+ href: string
+ isCurrent?: boolean
+}
+
interface BlockItem {
id: string
name: string
@@ -69,9 +86,13 @@ export function SearchModal({
open,
onOpenChange,
templates = [],
+ workflows = [],
+ workspaces = [],
loading = false,
+ isOnWorkflowPage = false,
}: SearchModalProps) {
const [searchQuery, setSearchQuery] = useState('')
+ const [selectedIndex, setSelectedIndex] = useState(0)
const params = useParams()
const router = useRouter()
const workspaceId = params.workspaceId as string
@@ -115,8 +136,10 @@ export function SearchModal({
}
}, [])
- // Get all available blocks
+ // Get all available blocks - only when on workflow page
const blocks = useMemo(() => {
+ if (!isOnWorkflowPage) return []
+
const allBlocks = getAllBlocks()
return allBlocks
.filter(
@@ -132,10 +155,12 @@ export function SearchModal({
})
)
.sort((a, b) => a.name.localeCompare(b.name))
- }, [])
+ }, [isOnWorkflowPage])
- // Get all available tools
+ // Get all available tools - only when on workflow page
const tools = useMemo(() => {
+ if (!isOnWorkflowPage) return []
+
const allBlocks = getAllBlocks()
return allBlocks
.filter((block) => block.category === 'tools')
@@ -149,7 +174,7 @@ export function SearchModal({
})
)
.sort((a, b) => a.name.localeCompare(b.name))
- }, [])
+ }, [isOnWorkflowPage])
// Define pages
const pages = useMemo(
@@ -230,6 +255,18 @@ export function SearchModal({
.slice(0, 8)
}, [localTemplates, searchQuery])
+ const filteredWorkflows = useMemo(() => {
+ if (!searchQuery.trim()) return workflows
+ const query = searchQuery.toLowerCase()
+ return workflows.filter((workflow) => workflow.name.toLowerCase().includes(query))
+ }, [workflows, searchQuery])
+
+ const filteredWorkspaces = useMemo(() => {
+ if (!searchQuery.trim()) return workspaces
+ const query = searchQuery.toLowerCase()
+ return workspaces.filter((workspace) => workspace.name.toLowerCase().includes(query))
+ }, [workspaces, searchQuery])
+
const filteredPages = useMemo(() => {
if (!searchQuery.trim()) return pages
const query = searchQuery.toLowerCase()
@@ -242,6 +279,42 @@ export function SearchModal({
return docs.filter((doc) => doc.name.toLowerCase().includes(query))
}, [docs, searchQuery])
+ // Create flattened list of navigatable items for keyboard navigation
+ const navigatableItems = useMemo(() => {
+ const items: Array<{
+ type: 'workspace' | 'workflow' | 'page' | 'doc'
+ data: any
+ section: string
+ }> = []
+
+ // Add workspaces
+ filteredWorkspaces.forEach((workspace) => {
+ items.push({ type: 'workspace', data: workspace, section: 'Workspaces' })
+ })
+
+ // Add workflows
+ filteredWorkflows.forEach((workflow) => {
+ items.push({ type: 'workflow', data: workflow, section: 'Workflows' })
+ })
+
+ // Add pages
+ filteredPages.forEach((page) => {
+ items.push({ type: 'page', data: page, section: 'Pages' })
+ })
+
+ // Add docs
+ filteredDocs.forEach((doc) => {
+ items.push({ type: 'doc', data: doc, section: 'Docs' })
+ })
+
+ return items
+ }, [filteredWorkspaces, filteredWorkflows, filteredPages, filteredDocs])
+
+ // Reset selected index when items change or modal opens
+ useEffect(() => {
+ setSelectedIndex(0)
+ }, [navigatableItems, open])
+
// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -292,6 +365,15 @@ export function SearchModal({
[router, onOpenChange]
)
+ // Handle workflow/workspace navigation (same as page navigation)
+ const handleNavigationClick = useCallback(
+ (href: string) => {
+ router.push(href)
+ onOpenChange(false)
+ },
+ [router, onOpenChange]
+ )
+
// Handle docs navigation
const handleDocsClick = useCallback(
(href: string) => {
@@ -360,6 +442,89 @@ export function SearchModal({
[]
)
+ // Handle item selection based on type
+ const handleItemSelection = useCallback(
+ (item: (typeof navigatableItems)[0]) => {
+ switch (item.type) {
+ case 'workspace':
+ if (item.data.isCurrent) {
+ onOpenChange(false)
+ } else {
+ handleNavigationClick(item.data.href)
+ }
+ break
+ case 'workflow':
+ if (item.data.isCurrent) {
+ onOpenChange(false)
+ } else {
+ handleNavigationClick(item.data.href)
+ }
+ break
+ case 'page':
+ handlePageClick(item.data.href)
+ break
+ case 'doc':
+ handleDocsClick(item.data.href)
+ break
+ }
+ },
+ [handleNavigationClick, handlePageClick, handleDocsClick, onOpenChange]
+ )
+
+ // Handle keyboard navigation
+ useEffect(() => {
+ if (!open) return
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault()
+ setSelectedIndex((prev) => Math.min(prev + 1, navigatableItems.length - 1))
+ break
+ case 'ArrowUp':
+ e.preventDefault()
+ setSelectedIndex((prev) => Math.max(prev - 1, 0))
+ break
+ case 'Enter':
+ e.preventDefault()
+ if (navigatableItems.length > 0 && selectedIndex < navigatableItems.length) {
+ const selectedItem = navigatableItems[selectedIndex]
+ handleItemSelection(selectedItem)
+ }
+ break
+ case 'Escape':
+ onOpenChange(false)
+ break
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [open, selectedIndex, navigatableItems, onOpenChange, handleItemSelection])
+
+ // Helper function to check if an item is selected
+ const isItemSelected = useCallback(
+ (item: any, itemType: string) => {
+ if (navigatableItems.length === 0 || selectedIndex >= navigatableItems.length) return false
+ const selectedItem = navigatableItems[selectedIndex]
+ return selectedItem.type === itemType && selectedItem.data.id === item.id
+ },
+ [navigatableItems, selectedIndex]
+ )
+
+ // Scroll selected item into view
+ useEffect(() => {
+ if (selectedIndex >= 0 && navigatableItems.length > 0) {
+ const selectedItem = navigatableItems[selectedIndex]
+ const itemElement = document.querySelector(
+ `[data-search-item="${selectedItem.type}-${selectedItem.data.id}"]`
+ )
+ if (itemElement) {
+ itemElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
+ }
+ }
+ }, [selectedIndex, navigatableItems])
+
// Render skeleton cards for loading state
const renderSkeletonCards = () => {
return Array.from({ length: 8 }).map((_, index) => (
@@ -560,6 +725,76 @@ export function SearchModal({
)}
+ {/* Workspaces Section */}
+ {filteredWorkspaces.length > 0 && (
+
+ )}
+
+ {/* Workflows Section */}
+ {filteredWorkflows.length > 0 && (
+
+ )}
+
{/* Pages Section */}
{filteredPages.length > 0 && (