improvement(ui/ux): workflow, search modal (#729)

* improvement: workflow colors

* fix: workflow rename styling

* improvement: no API call on no name change workspace after edit

* improvement: added workflow and workspace to search

* improvement: folder path opened for current workflow on load

* improvement: ui/ux workspace selector

* improvement: search modal keyboard use
This commit is contained in:
Emir Karabeg
2025-07-19 17:08:26 -07:00
committed by GitHub
parent ddefbaab38
commit 24c22537bb
10 changed files with 450 additions and 125 deletions

View File

@@ -135,43 +135,58 @@ interface TemplateCardProps {
// Skeleton component for loading states
export function TemplateCardSkeleton({ className }: { className?: string }) {
return (
<div className={cn('rounded-[14px] border bg-card shadow-xs', 'flex h-38', className)}>
<div className={cn('rounded-[14px] border bg-card shadow-xs', 'flex h-[142px]', className)}>
{/* Left side - Info skeleton */}
<div className='flex min-w-0 flex-1 flex-col justify-between p-4'>
{/* Top section skeleton */}
<div className='space-y-3'>
<div className='flex min-w-0 items-center gap-2.5'>
{/* Icon skeleton */}
<div className='h-5 w-5 flex-shrink-0 animate-pulse rounded bg-gray-200' />
{/* Title skeleton */}
<div className='h-4 w-24 animate-pulse rounded bg-gray-200' />
<div className='space-y-2'>
<div className='flex min-w-0 items-center justify-between gap-2.5'>
<div className='flex min-w-0 items-center gap-2.5'>
{/* Icon skeleton */}
<div className='h-5 w-5 flex-shrink-0 animate-pulse rounded-md bg-gray-200' />
{/* Title skeleton */}
<div className='h-4 w-32 animate-pulse rounded bg-gray-200' />
</div>
{/* Star and Use button skeleton */}
<div className='flex flex-shrink-0 items-center gap-3'>
<div className='h-4 w-4 animate-pulse rounded bg-gray-200' />
<div className='h-6 w-10 animate-pulse rounded-md bg-gray-200' />
</div>
</div>
{/* Description skeleton */}
<div className='space-y-2'>
<div className='space-y-1.5'>
<div className='h-3 w-full animate-pulse rounded bg-gray-200' />
<div className='h-3 w-3/4 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-1/2 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-4/5 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-3/5 animate-pulse rounded bg-gray-200' />
</div>
</div>
{/* Bottom section skeleton */}
<div className='flex min-w-0 items-center gap-1.5'>
<div className='h-3 w-8 animate-pulse rounded bg-gray-200' />
<div className='flex min-w-0 items-center gap-1.5 pt-1.5'>
<div className='h-3 w-6 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-16 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-1 animate-pulse rounded bg-gray-200' />
<div className='h-2 w-1 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-3 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-8 animate-pulse rounded bg-gray-200' />
{/* Stars section - hidden on smaller screens */}
<div className='hidden flex-shrink-0 items-center gap-1.5 sm:flex'>
<div className='h-2 w-1 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-3 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-200' />
</div>
</div>
</div>
{/* Right side - Blocks skeleton */}
<div className='flex w-16 flex-col gap-1 rounded-r-[14px] border-border border-l bg-secondary p-2'>
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className='flex items-center gap-1.5'>
<div className='h-3 w-3 animate-pulse rounded bg-gray-200' />
<div className='h-3 w-12 animate-pulse rounded bg-gray-200' />
</div>
{/* Right side - Block Icons skeleton */}
<div className='flex w-16 flex-col items-center justify-center gap-2 rounded-r-[14px] border-border border-l bg-secondary p-2'>
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className='animate-pulse rounded bg-gray-200'
style={{ width: '30px', height: '30px' }}
/>
))}
</div>
</div>

View File

@@ -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({
</div>
)}
{/* Workspaces Section */}
{filteredWorkspaces.length > 0 && (
<div>
<h3 className='mb-3 ml-6 font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
Workspaces
</h3>
<div className='space-y-1 px-6'>
{filteredWorkspaces.map((workspace) => (
<button
key={workspace.id}
onClick={() =>
workspace.isCurrent
? onOpenChange(false)
: handleNavigationClick(workspace.href)
}
data-search-item={`workspace-${workspace.id}`}
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
isItemSelected(workspace, 'workspace')
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent/60 focus:bg-accent/60'
}`}
>
<div className='flex h-5 w-5 items-center justify-center'>
<Building2 className='h-4 w-4 text-muted-foreground' />
</div>
<span className='flex-1 text-left font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
{workspace.name}
{workspace.isCurrent && ' (current)'}
</span>
</button>
))}
</div>
</div>
)}
{/* Workflows Section */}
{filteredWorkflows.length > 0 && (
<div>
<h3 className='mb-3 ml-6 font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
Workflows
</h3>
<div className='space-y-1 px-6'>
{filteredWorkflows.map((workflow) => (
<button
key={workflow.id}
onClick={() =>
workflow.isCurrent
? onOpenChange(false)
: handleNavigationClick(workflow.href)
}
data-search-item={`workflow-${workflow.id}`}
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
isItemSelected(workflow, 'workflow')
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent/60 focus:bg-accent/60'
}`}
>
<div className='flex h-5 w-5 items-center justify-center'>
<Workflow className='h-4 w-4 text-muted-foreground' />
</div>
<span className='flex-1 text-left font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
{workflow.name}
{workflow.isCurrent && ' (current)'}
</span>
</button>
))}
</div>
</div>
)}
{/* Pages Section */}
{filteredPages.length > 0 && (
<div>
@@ -571,7 +806,12 @@ export function SearchModal({
<button
key={page.id}
onClick={() => handlePageClick(page.href)}
className='flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-accent/60 focus:bg-accent/60 focus:outline-none'
data-search-item={`page-${page.id}`}
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
isItemSelected(page, 'page')
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent/60 focus:bg-accent/60'
}`}
>
<div className='flex h-5 w-5 items-center justify-center'>
<page.icon className='h-4 w-4 text-muted-foreground' />
@@ -605,7 +845,12 @@ export function SearchModal({
<button
key={doc.id}
onClick={() => handleDocsClick(doc.href)}
className='flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-accent/60 focus:bg-accent/60 focus:outline-none'
data-search-item={`doc-${doc.id}`}
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
isItemSelected(doc, 'doc')
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent/60 focus:bg-accent/60'
}`}
>
<div className='flex h-5 w-5 items-center justify-center'>
<doc.icon className='h-4 w-4 text-muted-foreground' />
@@ -622,11 +867,13 @@ export function SearchModal({
{/* Empty state */}
{searchQuery &&
!loading &&
filteredWorkflows.length === 0 &&
filteredWorkspaces.length === 0 &&
filteredPages.length === 0 &&
filteredDocs.length === 0 &&
filteredBlocks.length === 0 &&
filteredTools.length === 0 &&
filteredTemplates.length === 0 &&
filteredPages.length === 0 &&
filteredDocs.length === 0 && (
filteredTemplates.length === 0 && (
<div className='ml-6 py-12 text-center'>
<p className='text-muted-foreground'>No results found for "{searchQuery}"</p>
</div>

View File

@@ -14,7 +14,6 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store'
@@ -299,16 +298,20 @@ export function FolderItem({
</div>
{isEditing ? (
<Input
<input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className='h-6 flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
className='flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
maxLength={50}
disabled={isRenaming}
onClick={(e) => e.stopPropagation()} // Prevent folder toggle when clicking input
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
/>
) : (
<span className='flex-1 select-none truncate text-muted-foreground'>{folder.name}</span>

View File

@@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from 'react'
import clsx from 'clsx'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { useFolderStore, useIsWorkflowSelected } from '@/stores/folders/store'
@@ -220,16 +219,22 @@ export function WorkflowItem({
style={{ backgroundColor: workflow.color }}
/>
{isEditing ? (
<Input
<input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className='h-6 flex-1 border-0 bg-transparent p-0 text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
className={`flex-1 border-0 bg-transparent p-0 font-medium text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 ${
active && !isDragOver ? 'text-foreground' : 'text-muted-foreground'
}`}
maxLength={100}
disabled={isRenaming}
onClick={(e) => e.preventDefault()} // Prevent navigation when clicking input
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
/>
) : (
<span className='flex-1 select-none truncate'>

View File

@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import clsx from 'clsx'
import { useParams, usePathname } from 'next/navigation'
import { Skeleton } from '@/components/ui/skeleton'
@@ -354,6 +354,7 @@ export function FolderTree({
const pathname = usePathname()
const params = useParams()
const workspaceId = params.workspaceId as string
const workflowId = params.workflowId as string
const {
getFolderTree,
expandedFolders,
@@ -361,9 +362,33 @@ export function FolderTree({
isLoading: foldersLoading,
clearSelection,
updateFolderAPI,
getFolderPath,
setExpanded,
} = useFolderStore()
const { updateWorkflow } = useWorkflowRegistry()
// Memoize the active workflow's folder ID to avoid unnecessary re-runs
const activeWorkflowFolderId = useMemo(() => {
if (!workflowId || isLoading || foldersLoading) return null
const activeWorkflow = regularWorkflows.find((workflow) => workflow.id === workflowId)
return activeWorkflow?.folderId || null
}, [workflowId, regularWorkflows, isLoading, foldersLoading])
// Auto-expand folders when a workflow is active
useEffect(() => {
if (!activeWorkflowFolderId) return
// Get the folder path from root to the workflow's folder
const folderPath = getFolderPath(activeWorkflowFolderId)
// Expand all folders in the path (only if not already expanded)
folderPath.forEach((folder) => {
if (!expandedFolders.has(folder.id)) {
setExpanded(folder.id, true)
}
})
}, [activeWorkflowFolderId, getFolderPath, setExpanded])
// Clean up any existing folders with 3+ levels of nesting
const cleanupDeepNesting = useCallback(async () => {
const { getFolderTree, updateFolderAPI } = useFolderStore.getState()

View File

@@ -104,8 +104,9 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
case 'save': {
// Exit edit mode immediately, save in background
setIsEditingName(false)
if (activeWorkspace && editingName.trim() !== '') {
updateWorkspaceName(activeWorkspace.id, editingName.trim()).catch((error) => {
const trimmedName = editingName.trim()
if (activeWorkspace && trimmedName !== '' && trimmedName !== activeWorkspace.name) {
updateWorkspaceName(activeWorkspace.id, trimmedName).catch((error) => {
logger.error('Failed to update workspace name:', error)
})
}

View File

@@ -46,6 +46,7 @@ interface WorkspaceSelectorProps {
onLeaveWorkspace: (workspace: Workspace) => Promise<void>
isDeleting: boolean
isLeaving: boolean
isCreating: boolean
}
export function WorkspaceSelector({
@@ -59,6 +60,7 @@ export function WorkspaceSelector({
onLeaveWorkspace,
isDeleting,
isLeaving,
isCreating,
}: WorkspaceSelectorProps) {
const userPermissions = useUserPermissionsContext()
@@ -256,7 +258,7 @@ export function WorkspaceSelector({
onClick={userPermissions.canAdmin ? () => setShowInviteMembers(true) : undefined}
disabled={!userPermissions.canAdmin}
className={cn(
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs hover:bg-secondary hover:text-muted-foreground',
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs transition-colors hover:bg-muted-foreground/10 hover:text-muted-foreground',
!userPermissions.canAdmin && 'cursor-not-allowed opacity-50'
)}
>
@@ -269,7 +271,11 @@ export function WorkspaceSelector({
variant='secondary'
size='sm'
onClick={onCreateWorkspace}
className='h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs hover:bg-secondary hover:text-muted-foreground'
disabled={isCreating}
className={cn(
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs transition-colors hover:bg-muted-foreground/10 hover:text-muted-foreground',
isCreating && 'cursor-not-allowed'
)}
>
<Plus className='h-3 w-3' />
<span>Create</span>

View File

@@ -86,6 +86,8 @@ export function Sidebar() {
// Add state to prevent multiple simultaneous workflow creations
const [isCreatingWorkflow, setIsCreatingWorkflow] = useState(false)
// Add state to prevent multiple simultaneous workspace creations
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false)
// Add sidebar collapsed state
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
const params = useParams()
@@ -276,7 +278,13 @@ export function Sidebar() {
* Handle create workspace
*/
const handleCreateWorkspace = useCallback(async () => {
if (isCreatingWorkspace) {
logger.info('Workspace creation already in progress, ignoring request')
return
}
try {
setIsCreatingWorkspace(true)
logger.info('Creating new workspace')
const response = await fetch('/api/workspaces', {
@@ -306,8 +314,10 @@ export function Sidebar() {
await switchWorkspace(newWorkspace)
} catch (error) {
logger.error('Error creating workspace:', error)
} finally {
setIsCreatingWorkspace(false)
}
}, [refreshWorkspaceList, switchWorkspace])
}, [refreshWorkspaceList, switchWorkspace, isCreatingWorkspace])
/**
* Confirm delete workspace
@@ -570,6 +580,29 @@ export function Sidebar() {
return { regularWorkflows: regular, tempWorkflows: temp }
}, [workflows, isLoading, workspaceId])
// Prepare workflows for search modal
const searchWorkflows = useMemo(() => {
if (isLoading) return []
const allWorkflows = [...regularWorkflows, ...tempWorkflows]
return allWorkflows.map((workflow) => ({
id: workflow.id,
name: workflow.name,
href: `/workspace/${workspaceId}/w/${workflow.id}`,
isCurrent: workflow.id === workflowId,
}))
}, [regularWorkflows, tempWorkflows, workspaceId, workflowId, isLoading])
// Prepare workspaces for search modal (include all workspaces)
const searchWorkspaces = useMemo(() => {
return workspaces.map((workspace) => ({
id: workspace.id,
name: workspace.name,
href: `/workspace/${workspace.id}/w`,
isCurrent: workspace.id === workspaceId,
}))
}, [workspaces, workspaceId])
// Create workflow handler
const handleCreateWorkflow = async (folderId?: string): Promise<string> => {
if (isCreatingWorkflow) {
@@ -752,6 +785,7 @@ export function Sidebar() {
onLeaveWorkspace={handleLeaveWorkspace}
isDeleting={isDeleting}
isLeaving={isLeaving}
isCreating={isCreatingWorkspace}
/>
</div>
@@ -783,7 +817,7 @@ export function Sidebar() {
}`}
>
<div className='px-2'>
<ScrollArea ref={workflowScrollAreaRef} className='h-[212px]' hideScrollbar={true}>
<ScrollArea ref={workflowScrollAreaRef} className='h-[210px]' hideScrollbar={true}>
<FolderTree
regularWorkflows={regularWorkflows}
marketplaceWorkflows={tempWorkflows}
@@ -838,7 +872,15 @@ export function Sidebar() {
<SettingsModal open={showSettings} onOpenChange={setShowSettings} />
<HelpModal open={showHelp} onOpenChange={setShowHelp} />
<InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />
<SearchModal open={showSearchModal} onOpenChange={setShowSearchModal} templates={templates} />
<SearchModal
open={showSearchModal}
onOpenChange={setShowSearchModal}
templates={templates}
workflows={searchWorkflows}
workspaces={searchWorkspaces}
loading={isTemplatesLoading}
isOnWorkflowPage={isOnWorkflowPage}
/>
</>
)
}

View File

@@ -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(),

View File

@@ -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