diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index e3a5e85100..4c1c88973f 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -135,43 +135,58 @@ interface TemplateCardProps { // Skeleton component for loading states export function TemplateCardSkeleton({ className }: { className?: string }) { return ( -
+
{/* Left side - Info skeleton */}
{/* Top section skeleton */} -
-
- {/* Icon skeleton */} -
- {/* Title skeleton */} -
+
+
+
+ {/* 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 && ( +
+

+ Workspaces +

+
+ {filteredWorkspaces.map((workspace) => ( + + ))} +
+
+ )} + + {/* Workflows Section */} + {filteredWorkflows.length > 0 && ( +
+

+ Workflows +

+
+ {filteredWorkflows.map((workflow) => ( + + ))} +
+
+ )} + {/* Pages Section */} {filteredPages.length > 0 && (
@@ -571,7 +806,12 @@ export function SearchModal({
@@ -783,7 +817,7 @@ export function Sidebar() { }`} >
- + - + ) } diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 8e2b475b2b..36eed84bdc 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2808,7 +2808,7 @@ export const ResponseIcon = (props: SVGProps) => ( > ) diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index e7b56d726b..5ba497351c 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -159,7 +159,7 @@ export const workflowBlocks = pgTable( data: jsonb('data').default('{}'), parentId: text('parent_id'), - extent: text('extent'), // 'parent' or null + extent: text('extent'), // 'parent' or null or 'subflow' createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), diff --git a/apps/sim/stores/workflows/registry/utils.ts b/apps/sim/stores/workflows/registry/utils.ts index fd41d21a75..10df11c9ad 100644 --- a/apps/sim/stores/workflows/registry/utils.ts +++ b/apps/sim/stores/workflows/registry/utils.ts @@ -1,93 +1,74 @@ // Available workflow colors export const WORKFLOW_COLORS = [ - // Original colors - '#3972F6', // Blue - '#F639DD', // Pink/Magenta - '#F6B539', // Orange/Yellow - '#8139F6', // Purple - '#39B54A', // Green - '#39B5AB', // Teal - '#F66839', // Red/Orange + // Blues - vibrant blue tones + '#3972F6', // Blue (original) + '#2E5BF5', // Deeper Blue + '#1E4BF4', // Royal Blue + '#0D3BF3', // Deep Royal Blue - // Additional vibrant blues - '#2E5BFF', // Bright Blue - '#4A90FF', // Sky Blue - '#1E40AF', // Deep Blue - '#0EA5E9', // Cyan Blue - '#3B82F6', // Royal Blue - '#6366F1', // Indigo - '#1D4ED8', // Electric Blue + // Pinks/Magentas - vibrant pink and magenta tones + '#F639DD', // Pink/Magenta (original) + '#F529CF', // Deep Magenta + '#F749E7', // Light Magenta + '#F419C1', // Hot Pink - // Additional vibrant purples - '#A855F7', // Bright Purple - '#C084FC', // Light Purple - '#7C3AED', // Deep Purple - '#9333EA', // Violet - '#8B5CF6', // Medium Purple - '#6D28D9', // Dark Purple - '#5B21B6', // Deep Violet + // Oranges/Yellows - vibrant orange and yellow tones + '#F6B539', // Orange/Yellow (original) + '#F5A529', // Deep Orange + '#F49519', // Burnt Orange + '#F38509', // Deep Burnt Orange - // Additional vibrant pinks/magentas - '#EC4899', // Hot Pink - '#F97316', // Pink Orange - '#E11D48', // Rose - '#BE185D', // Deep Pink - '#DB2777', // Pink Red - '#F472B6', // Light Pink - '#F59E0B', // Amber Pink + // Purples - vibrant purple tones + '#8139F6', // Purple (original) + '#7129F5', // Deep Purple + '#6119F4', // Royal Purple + '#5109F3', // Deep Royal Purple - // Additional vibrant greens - '#10B981', // Emerald - '#059669', // Green Teal - '#16A34A', // Forest Green - '#22C55E', // Lime Green - '#84CC16', // Yellow Green - '#65A30D', // Olive Green - '#15803D', // Dark Green + // Greens - vibrant green tones + '#39B54A', // Green (original) + '#29A53A', // Deep Green + '#19952A', // Forest Green + '#09851A', // Deep Forest Green - // Additional vibrant teals/cyans - '#06B6D4', // Cyan - '#0891B2', // Dark Cyan - '#0E7490', // Teal Blue - '#14B8A6', // Turquoise - '#0D9488', // Dark Teal - '#047857', // Sea Green - '#059669', // Mint Green + // Teals/Cyans - vibrant teal and cyan tones + '#39B5AB', // Teal (original) + '#29A59B', // Deep Teal + '#19958B', // Dark Teal + '#09857B', // Deep Dark Teal - // Additional vibrant oranges/reds - '#EA580C', // Bright Orange - '#DC2626', // Red - '#B91C1C', // Dark Red - '#EF4444', // Light Red - '#F97316', // Orange - '#FB923C', // Light Orange - '#FDBA74', // Peach + // Reds/Red-Oranges - vibrant red and red-orange tones + '#F66839', // Red/Orange (original) + '#F55829', // Deep Red-Orange + '#F44819', // Burnt Red + '#F33809', // Deep Burnt Red - // Additional vibrant yellows/golds - '#FBBF24', // Gold - '#F59E0B', // Amber - '#D97706', // Dark Amber - '#92400E', // Bronze - '#EAB308', // Yellow - '#CA8A04', // Dark Yellow - '#A16207', // Mustard + // Additional vibrant colors for variety + // Corals - warm coral tones + '#F6397A', // Coral + '#F5296A', // Deep Coral + '#F7498A', // Light Coral - // Additional unique vibrant colors - '#FF6B6B', // Coral - '#4ECDC4', // Mint - '#45B7D1', // Light Blue - '#96CEB4', // Sage - '#FFEAA7', // Cream - '#DDA0DD', // Plum - '#98D8C8', // Seafoam - '#F7DC6F', // Banana - '#BB8FCE', // Lavender - '#85C1E9', // Baby Blue - '#F8C471', // Peach - '#82E0AA', // Light Green - '#F1948A', // Salmon - '#D7BDE2', // Lilac - '#D7BDE2', // Lilac + // Crimsons - deep red tones + '#DC143C', // Crimson + '#CC042C', // Deep Crimson + '#EC243C', // Light Crimson + '#BC003C', // Dark Crimson + '#FC343C', // Bright Crimson + + // Mint - fresh green tones + '#00FF7F', // Mint Green + '#00EF6F', // Deep Mint + '#00DF5F', // Dark Mint + + // Slate - blue-gray tones + '#6A5ACD', // Slate Blue + '#5A4ABD', // Deep Slate + '#4A3AAD', // Dark Slate + + // Amber - warm orange-yellow tones + '#FFBF00', // Amber + '#EFAF00', // Deep Amber + '#DF9F00', // Dark Amber ] // Random adjectives and nouns for generating creative workflow names